+ = $this->Text->autoParagraph(h(${{ singularVar }}->{{ field }})); ?> ++
From e9e35cc04f182202ee69f7923f435979daf95502 Mon Sep 17 00:00:00 2001
From: Benn Oshrin = $this->Paginator->counter(__('Page {{ '{{' }}page{{ '}}' }} of {{ '{{' }}pages{{ '}}' }}, showing {{ '{{' }}current{{ '}}' }} record(s) out of {{ '{{' }}count{{ '}}' }} total')) ?>new Filesystem()
+ findRecursive
+ new Filesystem()
+ find
+ = __('{{ pluralHumanName }}') ?>
+
+
+
+
+{% for field in fields %}
+
+
+
+
+ = $this->Paginator->sort('{{ field }}') ?>
+{% endfor %}
+ = __('Actions') ?>
+
+{% for field in fields %}
+{% set isKey = false %}
+{% if associations.BelongsTo is defined %}
+{% for alias, details in associations.BelongsTo %}
+{% if field == details.foreignKey %}
+{% set isKey = true %}
+
+
+
+ = ${{ singularVar }}->has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if isKey is not same as(true) %}
+{% set columnData = Bake.columnData(field, schema) %}
+{% if columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %}
+ = h(${{ singularVar }}->{{ field }}) ?>
+{% else %}
+ = $this->Number->format(${{ singularVar }}->{{ field }}) ?>
+{% endif %}
+{% endif %}
+{% endfor %}
+{% set pk = '$' ~ singularVar ~ '->' ~ primaryKey[0] %}
+
+ = $this->Html->link(__('View'), ['action' => 'view', {{ pk|raw }}]) ?>
+ = $this->Html->link(__('Edit'), ['action' => 'edit', {{ pk|raw }}]) ?>
+ = $this->Form->postLink(__('Delete'), ['action' => 'delete', {{ pk|raw }}], ['confirm' => __('Are you sure you want to delete # {0}?', {{ pk|raw }})]) ?>
+
+
+ = $this->Paginator->first('<< ' . __('first')) ?>
+ = $this->Paginator->prev('< ' . __('previous')) ?>
+ = $this->Paginator->numbers() ?>
+ = $this->Paginator->next(__('next') . ' >') ?>
+ = $this->Paginator->last(__('last') . ' >>') ?>
+
+ = h(${{ singularVar }}->{{ displayField }}) ?>
+
+{% if groupedFields['string'] %}
+{% for field in groupedFields['string'] %}
+{% if associationFields[field] is defined %}
+{% set details = associationFields[field] %}
+
+{% if groupedFields.text %}
+{% for field in groupedFields.text %}
+
+
+{% else %}
+ = __('{{ details.property|humanize }}') ?>
+ = ${{ singularVar }}->has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
+
+
+{% endif %}
+{% endfor %}
+{% endif %}
+{% if associations.HasOne %}
+{% for alias, details in associations.HasOne %}
+ = __('{{ field|humanize }}') ?>
+ = h(${{ singularVar }}->{{ field }}) ?>
+
+
+{% endfor %}
+{% endif %}
+{% if groupedFields.number %}
+{% for field in groupedFields.number %}
+ = __('{{ alias|underscore|singularize|humanize }}') ?>
+ = ${{ singularVar }}->has('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
+
+
+{% endfor %}
+{% endif %}
+{% if groupedFields.date %}
+{% for field in groupedFields.date %}
+ = __('{{ field|humanize }}') ?>
+ = $this->Number->format(${{ singularVar }}->{{ field }}) ?>
+
+
+{% endfor %}
+{% endif %}
+{% if groupedFields.boolean %}
+{% for field in groupedFields.boolean %}
+ = __('{{ field|humanize }}') ?>
+ = h(${{ singularVar }}->{{ field }}) ?>
+
+
+{% endfor %}
+{% endif %}
+ = __('{{ field|humanize }}') ?>
+ = ${{ singularVar }}->{{ field }} ? __('Yes') : __('No'); ?>
+
+ = $this->Text->autoParagraph(h(${{ singularVar }}->{{ field }})); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
` tags around the output of given variable. Similar to debug().
+ *
+ * This function returns the same variable that was passed.
+ *
+ * @param mixed $var Variable to print out.
+ * @return mixed the same $var that was passed to this function
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pr
+ * @see debug()
+ */
+ function pr($var)
+ {
+ if (!Configure::read('debug')) {
+ return $var;
+ }
+
+ $template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '%s
' : "\n%s\n\n";
+ printf($template, trim(print_r($var, true)));
+
+ return $var;
+ }
+
+}
+
+if (!function_exists('pj')) {
+ /**
+ * JSON pretty print convenience function.
+ *
+ * In terminals this will act similar to using json_encode() with JSON_PRETTY_PRINT directly, when not run on CLI
+ * will also wrap `` tags around the output of given variable. Similar to pr().
+ *
+ * This function returns the same variable that was passed.
+ *
+ * @param mixed $var Variable to print out.
+ * @return mixed the same $var that was passed to this function
+ * @see pr()
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#pj
+ */
+ function pj($var)
+ {
+ if (!Configure::read('debug')) {
+ return $var;
+ }
+
+ $template = PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ? '%s
' : "\n%s\n\n";
+ printf($template, trim(json_encode($var, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)));
+
+ return $var;
+ }
+
+}
+
+if (!function_exists('env')) {
+ /**
+ * Gets an environment variable from available sources, and provides emulation
+ * for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on
+ * IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom
+ * environment information.
+ *
+ * @param string $key Environment variable name.
+ * @param string|bool|null $default Specify a default value in case the environment variable is not defined.
+ * @return string|bool|null Environment variable setting.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#env
+ */
+ function env(string $key, $default = null)
+ {
+ if ($key === 'HTTPS') {
+ if (isset($_SERVER['HTTPS'])) {
+ return !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
+ }
+
+ return strpos((string)env('SCRIPT_URI'), 'https://') === 0;
+ }
+
+ if ($key === 'SCRIPT_NAME' && env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) {
+ $key = 'SCRIPT_URL';
+ }
+
+ $val = null;
+ if (isset($_SERVER[$key])) {
+ $val = $_SERVER[$key];
+ } elseif (isset($_ENV[$key])) {
+ $val = $_ENV[$key];
+ } elseif (getenv($key) !== false) {
+ $val = getenv($key);
+ }
+
+ if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) {
+ $addr = env('HTTP_PC_REMOTE_ADDR');
+ if ($addr !== null) {
+ $val = $addr;
+ }
+ }
+
+ if ($val !== null) {
+ return $val;
+ }
+
+ switch ($key) {
+ case 'DOCUMENT_ROOT':
+ $name = (string)env('SCRIPT_NAME');
+ $filename = (string)env('SCRIPT_FILENAME');
+ $offset = 0;
+ if (!strpos($name, '.php')) {
+ $offset = 4;
+ }
+
+ return substr($filename, 0, -(strlen($name) + $offset));
+ case 'PHP_SELF':
+ return str_replace((string)env('DOCUMENT_ROOT'), '', (string)env('SCRIPT_FILENAME'));
+ case 'CGI_MODE':
+ return PHP_SAPI === 'cgi';
+ }
+
+ return $default;
+ }
+
+}
+
+if (!function_exists('triggerWarning')) {
+ /**
+ * Triggers an E_USER_WARNING.
+ *
+ * @param string $message The warning message.
+ * @return void
+ */
+ function triggerWarning(string $message): void
+ {
+ $stackFrame = 1;
+ $trace = debug_backtrace();
+ if (isset($trace[$stackFrame])) {
+ $frame = $trace[$stackFrame];
+ $frame += ['file' => '[internal]', 'line' => '??'];
+ $message = sprintf(
+ '%s - %s, line: %s',
+ $message,
+ $frame['file'],
+ $frame['line']
+ );
+ }
+ trigger_error($message, E_USER_WARNING);
+ }
+}
+
+if (!function_exists('deprecationWarning')) {
+ /**
+ * Helper method for outputting deprecation warnings
+ *
+ * @param string $message The message to output as a deprecation warning.
+ * @param int $stackFrame The stack frame to include in the error. Defaults to 1
+ * as that should point to application/plugin code.
+ * @return void
+ */
+ function deprecationWarning(string $message, int $stackFrame = 1): void
+ {
+ if (!(error_reporting() & E_USER_DEPRECATED)) {
+ return;
+ }
+
+ $trace = debug_backtrace();
+ if (isset($trace[$stackFrame])) {
+ $frame = $trace[$stackFrame];
+ $frame += ['file' => '[internal]', 'line' => '??'];
+
+ $relative = str_replace(DIRECTORY_SEPARATOR, '/', substr($frame['file'], strlen(ROOT) + 1));
+ $patterns = (array)Configure::read('Error.ignoredDeprecationPaths');
+ foreach ($patterns as $pattern) {
+ $pattern = str_replace(DIRECTORY_SEPARATOR, '/', $pattern);
+ if (fnmatch($pattern, $relative)) {
+ return;
+ }
+ }
+
+ $message = sprintf(
+ '%s - %s, line: %s' . "\n" .
+ ' You can disable all deprecation warnings by setting `Error.errorLevel` to' .
+ ' `E_ALL & ~E_USER_DEPRECATED`, or add `%s` to ' .
+ ' `Error.ignoredDeprecationPaths` in your `config/app.php` to mute deprecations from only this file.',
+ $message,
+ $frame['file'],
+ $frame['line'],
+ $relative
+ );
+ }
+
+ trigger_error($message, E_USER_DEPRECATED);
+ }
+}
+
+if (!function_exists('getTypeName')) {
+ /**
+ * Returns the objects class or var type of it's not an object
+ *
+ * @param mixed $var Variable to check
+ * @return string Returns the class name or variable type
+ */
+ function getTypeName($var): string
+ {
+ return is_object($var) ? get_class($var) : gettype($var);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Connection.php b/app/vendor/cakephp/cakephp/src/Database/Connection.php
new file mode 100644
index 000000000..baa957054
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Connection.php
@@ -0,0 +1,960 @@
+_config = $config;
+
+ $driver = '';
+ if (!empty($config['driver'])) {
+ $driver = $config['driver'];
+ }
+ $this->setDriver($driver, $config);
+
+ if (!empty($config['log'])) {
+ $this->enableQueryLogging((bool)$config['log']);
+ }
+ }
+
+ /**
+ * Destructor
+ *
+ * Disconnects the driver to release the connection.
+ */
+ public function __destruct()
+ {
+ if ($this->_transactionStarted && class_exists(Log::class)) {
+ Log::warning('The connection is going to be closed but there is an active transaction.');
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function config(): array
+ {
+ return $this->_config;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function configName(): string
+ {
+ if (empty($this->_config['name'])) {
+ return '';
+ }
+
+ return $this->_config['name'];
+ }
+
+ /**
+ * Sets the driver instance. If a string is passed it will be treated
+ * as a class name and will be instantiated.
+ *
+ * @param \Cake\Database\DriverInterface|string $driver The driver instance to use.
+ * @param array $config Config for a new driver.
+ * @throws \Cake\Database\Exception\MissingDriverException When a driver class is missing.
+ * @throws \Cake\Database\Exception\MissingExtensionException When a driver's PHP extension is missing.
+ * @return $this
+ */
+ public function setDriver($driver, $config = [])
+ {
+ if (is_string($driver)) {
+ /** @psalm-var class-string<\Cake\Database\DriverInterface>|null $className */
+ $className = App::className($driver, 'Database/Driver');
+ if ($className === null) {
+ throw new MissingDriverException(['driver' => $driver]);
+ }
+ $driver = new $className($config);
+ }
+ if (!$driver->enabled()) {
+ throw new MissingExtensionException(['driver' => get_class($driver)]);
+ }
+
+ $this->_driver = $driver;
+
+ return $this;
+ }
+
+ /**
+ * Get the retry wrapper object that is allows recovery from server disconnects
+ * while performing certain database actions, such as executing a query.
+ *
+ * @return \Cake\Core\Retry\CommandRetry The retry wrapper
+ */
+ public function getDisconnectRetry(): CommandRetry
+ {
+ return new CommandRetry(new ReconnectStrategy($this));
+ }
+
+ /**
+ * Gets the driver instance.
+ *
+ * @return \Cake\Database\DriverInterface
+ */
+ public function getDriver(): DriverInterface
+ {
+ return $this->_driver;
+ }
+
+ /**
+ * Connects to the configured database.
+ *
+ * @throws \Cake\Database\Exception\MissingConnectionException If database connection could not be established.
+ * @return bool true, if the connection was already established or the attempt was successful.
+ */
+ public function connect(): bool
+ {
+ try {
+ return $this->_driver->connect();
+ } catch (MissingConnectionException $e) {
+ throw $e;
+ } catch (Throwable $e) {
+ throw new MissingConnectionException(
+ [
+ 'driver' => App::shortName(get_class($this->_driver), 'Database/Driver'),
+ 'reason' => $e->getMessage(),
+ ],
+ null,
+ $e
+ );
+ }
+ }
+
+ /**
+ * Disconnects from database server.
+ *
+ * @return void
+ */
+ public function disconnect(): void
+ {
+ $this->_driver->disconnect();
+ }
+
+ /**
+ * Returns whether connection to database server was already established.
+ *
+ * @return bool
+ */
+ public function isConnected(): bool
+ {
+ return $this->_driver->isConnected();
+ }
+
+ /**
+ * Prepares a SQL statement to be executed.
+ *
+ * @param string|\Cake\Database\Query $query The SQL to convert into a prepared statement.
+ * @return \Cake\Database\StatementInterface
+ */
+ public function prepare($query): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($query) {
+ $statement = $this->_driver->prepare($query);
+
+ if ($this->_logQueries) {
+ $statement = $this->_newLogger($statement);
+ }
+
+ return $statement;
+ });
+ }
+
+ /**
+ * Executes a query using $params for interpolating values and $types as a hint for each
+ * those params.
+ *
+ * @param string $sql SQL to be executed and interpolated with $params
+ * @param array $params list or associative array of params to be interpolated in $sql as values
+ * @param array $types list or associative array of types to be used for casting values in query
+ * @return \Cake\Database\StatementInterface executed statement
+ */
+ public function execute(string $sql, array $params = [], array $types = []): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($sql, $params, $types) {
+ $statement = $this->prepare($sql);
+ if (!empty($params)) {
+ $statement->bind($params, $types);
+ }
+ $statement->execute();
+
+ return $statement;
+ });
+ }
+
+ /**
+ * Compiles a Query object into a SQL string according to the dialect for this
+ * connection's driver
+ *
+ * @param \Cake\Database\Query $query The query to be compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder
+ * @return string
+ */
+ public function compileQuery(Query $query, ValueBinder $binder): string
+ {
+ return $this->getDriver()->compileQuery($query, $binder)[1];
+ }
+
+ /**
+ * Executes the provided query after compiling it for the specific driver
+ * dialect and returns the executed Statement object.
+ *
+ * @param \Cake\Database\Query $query The query to be executed
+ * @return \Cake\Database\StatementInterface executed statement
+ */
+ public function run(Query $query): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($query) {
+ $statement = $this->prepare($query);
+ $query->getValueBinder()->attachTo($statement);
+ $statement->execute();
+
+ return $statement;
+ });
+ }
+
+ /**
+ * Executes a SQL statement and returns the Statement object as result.
+ *
+ * @param string $sql The SQL query to execute.
+ * @return \Cake\Database\StatementInterface
+ */
+ public function query(string $sql): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($sql) {
+ $statement = $this->prepare($sql);
+ $statement->execute();
+
+ return $statement;
+ });
+ }
+
+ /**
+ * Create a new Query instance for this connection.
+ *
+ * @return \Cake\Database\Query
+ */
+ public function newQuery(): Query
+ {
+ return new Query($this);
+ }
+
+ /**
+ * Sets a Schema\Collection object for this connection.
+ *
+ * @param \Cake\Database\Schema\CollectionInterface $collection The schema collection object
+ * @return $this
+ */
+ public function setSchemaCollection(SchemaCollectionInterface $collection)
+ {
+ $this->_schemaCollection = $collection;
+
+ return $this;
+ }
+
+ /**
+ * Gets a Schema\Collection object for this connection.
+ *
+ * @return \Cake\Database\Schema\CollectionInterface
+ */
+ public function getSchemaCollection(): SchemaCollectionInterface
+ {
+ if ($this->_schemaCollection !== null) {
+ return $this->_schemaCollection;
+ }
+
+ if (!empty($this->_config['cacheMetadata'])) {
+ return $this->_schemaCollection = new CachedCollection(
+ new SchemaCollection($this),
+ empty($this->_config['cacheKeyPrefix']) ? $this->configName() : $this->_config['cacheKeyPrefix'],
+ $this->getCacher()
+ );
+ }
+
+ return $this->_schemaCollection = new SchemaCollection($this);
+ }
+
+ /**
+ * Executes an INSERT query on the specified table.
+ *
+ * @param string $table the table to insert values in
+ * @param array $values values to be inserted
+ * @param array $types list of associative array containing the types to be used for casting
+ * @return \Cake\Database\StatementInterface
+ */
+ public function insert(string $table, array $values, array $types = []): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($table, $values, $types) {
+ $columns = array_keys($values);
+
+ return $this->newQuery()->insert($columns, $types)
+ ->into($table)
+ ->values($values)
+ ->execute();
+ });
+ }
+
+ /**
+ * Executes an UPDATE statement on the specified table.
+ *
+ * @param string $table the table to update rows from
+ * @param array $values values to be updated
+ * @param array $conditions conditions to be set for update statement
+ * @param array $types list of associative array containing the types to be used for casting
+ * @return \Cake\Database\StatementInterface
+ */
+ public function update(string $table, array $values, array $conditions = [], array $types = []): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($table, $values, $conditions, $types) {
+ return $this->newQuery()->update($table)
+ ->set($values, $types)
+ ->where($conditions, $types)
+ ->execute();
+ });
+ }
+
+ /**
+ * Executes a DELETE statement on the specified table.
+ *
+ * @param string $table the table to delete rows from
+ * @param array $conditions conditions to be set for delete statement
+ * @param array $types list of associative array containing the types to be used for casting
+ * @return \Cake\Database\StatementInterface
+ */
+ public function delete(string $table, array $conditions = [], array $types = []): StatementInterface
+ {
+ return $this->getDisconnectRetry()->run(function () use ($table, $conditions, $types) {
+ return $this->newQuery()->delete($table)
+ ->where($conditions, $types)
+ ->execute();
+ });
+ }
+
+ /**
+ * Starts a new transaction.
+ *
+ * @return void
+ */
+ public function begin(): void
+ {
+ if (!$this->_transactionStarted) {
+ if ($this->_logQueries) {
+ $this->log('BEGIN');
+ }
+
+ $this->getDisconnectRetry()->run(function (): void {
+ $this->_driver->beginTransaction();
+ });
+
+ $this->_transactionLevel = 0;
+ $this->_transactionStarted = true;
+ $this->nestedTransactionRollbackException = null;
+
+ return;
+ }
+
+ $this->_transactionLevel++;
+ if ($this->isSavePointsEnabled()) {
+ $this->createSavePoint((string)$this->_transactionLevel);
+ }
+ }
+
+ /**
+ * Commits current transaction.
+ *
+ * @return bool true on success, false otherwise
+ */
+ public function commit(): bool
+ {
+ if (!$this->_transactionStarted) {
+ return false;
+ }
+
+ if ($this->_transactionLevel === 0) {
+ if ($this->wasNestedTransactionRolledback()) {
+ /** @var \Cake\Database\Exception\NestedTransactionRollbackException $e */
+ $e = $this->nestedTransactionRollbackException;
+ $this->nestedTransactionRollbackException = null;
+ throw $e;
+ }
+
+ $this->_transactionStarted = false;
+ $this->nestedTransactionRollbackException = null;
+ if ($this->_logQueries) {
+ $this->log('COMMIT');
+ }
+
+ return $this->_driver->commitTransaction();
+ }
+ if ($this->isSavePointsEnabled()) {
+ $this->releaseSavePoint((string)$this->_transactionLevel);
+ }
+
+ $this->_transactionLevel--;
+
+ return true;
+ }
+
+ /**
+ * Rollback current transaction.
+ *
+ * @param bool|null $toBeginning Whether or not the transaction should be rolled back to the
+ * beginning of it. Defaults to false if using savepoints, or true if not.
+ * @return bool
+ */
+ public function rollback(?bool $toBeginning = null): bool
+ {
+ if (!$this->_transactionStarted) {
+ return false;
+ }
+
+ $useSavePoint = $this->isSavePointsEnabled();
+ if ($toBeginning === null) {
+ $toBeginning = !$useSavePoint;
+ }
+ if ($this->_transactionLevel === 0 || $toBeginning) {
+ $this->_transactionLevel = 0;
+ $this->_transactionStarted = false;
+ $this->nestedTransactionRollbackException = null;
+ if ($this->_logQueries) {
+ $this->log('ROLLBACK');
+ }
+ $this->_driver->rollbackTransaction();
+
+ return true;
+ }
+
+ $savePoint = $this->_transactionLevel--;
+ if ($useSavePoint) {
+ $this->rollbackSavepoint($savePoint);
+ } elseif ($this->nestedTransactionRollbackException === null) {
+ $this->nestedTransactionRollbackException = new NestedTransactionRollbackException();
+ }
+
+ return true;
+ }
+
+ /**
+ * Enables/disables the usage of savepoints, enables only if driver the allows it.
+ *
+ * If you are trying to enable this feature, make sure you check
+ * `isSavePointsEnabled()` to verify that savepoints were enabled successfully.
+ *
+ * @param bool $enable Whether or not save points should be used.
+ * @return $this
+ */
+ public function enableSavePoints(bool $enable = true)
+ {
+ if ($enable === false) {
+ $this->_useSavePoints = false;
+ } else {
+ $this->_useSavePoints = $this->_driver->supportsSavePoints();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Disables the usage of savepoints.
+ *
+ * @return $this
+ */
+ public function disableSavePoints()
+ {
+ $this->_useSavePoints = false;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether this connection is using savepoints for nested transactions
+ *
+ * @return bool true if enabled, false otherwise
+ */
+ public function isSavePointsEnabled(): bool
+ {
+ return $this->_useSavePoints;
+ }
+
+ /**
+ * Creates a new save point for nested transactions.
+ *
+ * @param string|int $name Save point name or id
+ * @return void
+ */
+ public function createSavePoint($name): void
+ {
+ $this->execute($this->_driver->savePointSQL($name))->closeCursor();
+ }
+
+ /**
+ * Releases a save point by its name.
+ *
+ * @param string|int $name Save point name or id
+ * @return void
+ */
+ public function releaseSavePoint($name): void
+ {
+ $this->execute($this->_driver->releaseSavePointSQL($name))->closeCursor();
+ }
+
+ /**
+ * Rollback a save point by its name.
+ *
+ * @param string|int $name Save point name or id
+ * @return void
+ */
+ public function rollbackSavepoint($name): void
+ {
+ $this->execute($this->_driver->rollbackSavePointSQL($name))->closeCursor();
+ }
+
+ /**
+ * Run driver specific SQL to disable foreign key checks.
+ *
+ * @return void
+ */
+ public function disableForeignKeys(): void
+ {
+ $this->getDisconnectRetry()->run(function (): void {
+ $this->execute($this->_driver->disableForeignKeySQL())->closeCursor();
+ });
+ }
+
+ /**
+ * Run driver specific SQL to enable foreign key checks.
+ *
+ * @return void
+ */
+ public function enableForeignKeys(): void
+ {
+ $this->getDisconnectRetry()->run(function (): void {
+ $this->execute($this->_driver->enableForeignKeySQL())->closeCursor();
+ });
+ }
+
+ /**
+ * Returns whether the driver supports adding or dropping constraints
+ * to already created tables.
+ *
+ * @return bool true if driver supports dynamic constraints
+ */
+ public function supportsDynamicConstraints(): bool
+ {
+ return $this->_driver->supportsDynamicConstraints();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function transactional(callable $callback)
+ {
+ $this->begin();
+
+ try {
+ $result = $callback($this);
+ } catch (Throwable $e) {
+ $this->rollback(false);
+ throw $e;
+ }
+
+ if ($result === false) {
+ $this->rollback(false);
+
+ return false;
+ }
+
+ try {
+ $this->commit();
+ } catch (NestedTransactionRollbackException $e) {
+ $this->rollback(false);
+ throw $e;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns whether some nested transaction has been already rolled back.
+ *
+ * @return bool
+ */
+ protected function wasNestedTransactionRolledback(): bool
+ {
+ return $this->nestedTransactionRollbackException instanceof NestedTransactionRollbackException;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function disableConstraints(callable $callback)
+ {
+ return $this->getDisconnectRetry()->run(function () use ($callback) {
+ $this->disableForeignKeys();
+
+ try {
+ $result = $callback($this);
+ } finally {
+ $this->enableForeignKeys();
+ }
+
+ return $result;
+ });
+ }
+
+ /**
+ * Checks if a transaction is running.
+ *
+ * @return bool True if a transaction is running else false.
+ */
+ public function inTransaction(): bool
+ {
+ return $this->_transactionStarted;
+ }
+
+ /**
+ * Quotes value to be used safely in database query.
+ *
+ * @param mixed $value The value to quote.
+ * @param string|int|\Cake\Database\TypeInterface $type Type to be used for determining kind of quoting to perform
+ * @return string Quoted value
+ */
+ public function quote($value, $type = 'string'): string
+ {
+ [$value, $type] = $this->cast($value, $type);
+
+ return $this->_driver->quote($value, $type);
+ }
+
+ /**
+ * Checks if the driver supports quoting.
+ *
+ * @return bool
+ */
+ public function supportsQuoting(): bool
+ {
+ return $this->_driver->supportsQuoting();
+ }
+
+ /**
+ * Quotes a database identifier (a column name, table name, etc..) to
+ * be used safely in queries without the risk of using reserved words.
+ *
+ * @param string $identifier The identifier to quote.
+ * @return string
+ */
+ public function quoteIdentifier(string $identifier): string
+ {
+ return $this->_driver->quoteIdentifier($identifier);
+ }
+
+ /**
+ * Enables or disables metadata caching for this connection
+ *
+ * Changing this setting will not modify existing schema collections objects.
+ *
+ * @param bool|string $cache Either boolean false to disable metadata caching, or
+ * true to use `_cake_model_` or the name of the cache config to use.
+ * @return void
+ */
+ public function cacheMetadata($cache): void
+ {
+ $this->_schemaCollection = null;
+ $this->_config['cacheMetadata'] = $cache;
+ if (is_string($cache)) {
+ $this->cacher = null;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setCacher(CacheInterface $cacher)
+ {
+ $this->cacher = $cacher;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getCacher(): CacheInterface
+ {
+ if ($this->cacher !== null) {
+ return $this->cacher;
+ }
+
+ $configName = $this->_config['cacheMetadata'] ?? '_cake_model_';
+ if (!is_string($configName)) {
+ $configName = '_cake_model_';
+ }
+
+ if (!class_exists(Cache::class)) {
+ throw new RuntimeException(
+ 'To use caching you must either set a cacher using Connection::setCacher()' .
+ ' or require the cakephp/cache package in your composer config.'
+ );
+ }
+
+ return $this->cacher = Cache::pool($configName);
+ }
+
+ /**
+ * Enable/disable query logging
+ *
+ * @param bool $enable Enable/disable query logging
+ * @return $this
+ */
+ public function enableQueryLogging(bool $enable = true)
+ {
+ $this->_logQueries = $enable;
+
+ return $this;
+ }
+
+ /**
+ * Disable query logging
+ *
+ * @return $this
+ */
+ public function disableQueryLogging()
+ {
+ $this->_logQueries = false;
+
+ return $this;
+ }
+
+ /**
+ * Check if query logging is enabled.
+ *
+ * @return bool
+ */
+ public function isQueryLoggingEnabled(): bool
+ {
+ return $this->_logQueries;
+ }
+
+ /**
+ * Sets a logger
+ *
+ * @param \Psr\Log\LoggerInterface $logger Logger object
+ * @return $this
+ * @psalm-suppress ImplementedReturnTypeMismatch
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->_logger = $logger;
+
+ return $this;
+ }
+
+ /**
+ * Gets the logger object
+ *
+ * @return \Psr\Log\LoggerInterface logger instance
+ */
+ public function getLogger(): LoggerInterface
+ {
+ if ($this->_logger !== null) {
+ return $this->_logger;
+ }
+
+ if (!class_exists(QueryLogger::class)) {
+ throw new RuntimeException(
+ 'For logging you must either set a logger using Connection::setLogger()' .
+ ' or require the cakephp/log package in your composer config.'
+ );
+ }
+
+ return $this->_logger = new QueryLogger(['connection' => $this->configName()]);
+ }
+
+ /**
+ * Logs a Query string using the configured logger object.
+ *
+ * @param string $sql string to be logged
+ * @return void
+ */
+ public function log(string $sql): void
+ {
+ $query = new LoggedQuery();
+ $query->query = $sql;
+ $this->getLogger()->debug((string)$query, ['query' => $query]);
+ }
+
+ /**
+ * Returns a new statement object that will log the activity
+ * for the passed original statement instance.
+ *
+ * @param \Cake\Database\StatementInterface $statement the instance to be decorated
+ * @return \Cake\Database\Log\LoggingStatement
+ */
+ protected function _newLogger(StatementInterface $statement): LoggingStatement
+ {
+ $log = new LoggingStatement($statement, $this->_driver);
+ $log->setLogger($this->getLogger());
+
+ return $log;
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $secrets = [
+ 'password' => '*****',
+ 'username' => '*****',
+ 'host' => '*****',
+ 'database' => '*****',
+ 'port' => '*****',
+ ];
+ $replace = array_intersect_key($secrets, $this->_config);
+ $config = $replace + $this->_config;
+
+ return [
+ 'config' => $config,
+ 'driver' => $this->_driver,
+ 'transactionLevel' => $this->_transactionLevel,
+ 'transactionStarted' => $this->_transactionStarted,
+ 'useSavePoints' => $this->_useSavePoints,
+ 'logQueries' => $this->_logQueries,
+ 'logger' => $this->_logger,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/ConstraintsInterface.php b/app/vendor/cakephp/cakephp/src/Database/ConstraintsInterface.php
new file mode 100644
index 000000000..f1fe3c161
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/ConstraintsInterface.php
@@ -0,0 +1,49 @@
+_baseConfig;
+ $this->_config = $config;
+ if (!empty($config['quoteIdentifiers'])) {
+ $this->enableAutoQuoting();
+ }
+ }
+
+ /**
+ * Establishes a connection to the database server
+ *
+ * @param string $dsn A Driver-specific PDO-DSN
+ * @param array $config configuration to be used for creating connection
+ * @return bool true on success
+ */
+ protected function _connect(string $dsn, array $config): bool
+ {
+ $action = function () use ($dsn, $config) {
+ $this->setConnection(new PDO(
+ $dsn,
+ $config['username'] ?: null,
+ $config['password'] ?: null,
+ $config['flags']
+ ));
+ };
+
+ $retry = new CommandRetry(new ErrorCodeWaitStrategy(static::RETRY_ERROR_CODES, 5), 4);
+ try {
+ $retry->run($action);
+ } catch (PDOException $e) {
+ throw new MissingConnectionException(
+ [
+ 'driver' => App::shortName(static::class, 'Database/Driver'),
+ 'reason' => $e->getMessage(),
+ ],
+ null,
+ $e
+ );
+ } finally {
+ $this->connectRetries = $retry->getRetries();
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ abstract public function connect(): bool;
+
+ /**
+ * @inheritDoc
+ */
+ public function disconnect(): void
+ {
+ /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
+ $this->_connection = null;
+ $this->_version = null;
+ }
+
+ /**
+ * Returns connected server version.
+ *
+ * @return string
+ */
+ public function version(): string
+ {
+ if ($this->_version === null) {
+ $this->connect();
+ $this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
+ }
+
+ return $this->_version;
+ }
+
+ /**
+ * Get the internal PDO connection instance.
+ *
+ * @return \PDO
+ */
+ public function getConnection()
+ {
+ if ($this->_connection === null) {
+ throw new MissingConnectionException([
+ 'driver' => App::shortName(static::class, 'Database/Driver'),
+ 'reason' => 'Unknown',
+ ]);
+ }
+
+ return $this->_connection;
+ }
+
+ /**
+ * Set the internal PDO connection instance.
+ *
+ * @param \PDO $connection PDO instance.
+ * @return $this
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ public function setConnection($connection)
+ {
+ $this->_connection = $connection;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ abstract public function enabled(): bool;
+
+ /**
+ * @inheritDoc
+ */
+ public function prepare($query): StatementInterface
+ {
+ $this->connect();
+ $statement = $this->_connection->prepare($query instanceof Query ? $query->sql() : $query);
+
+ return new PDOStatement($statement, $this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function beginTransaction(): bool
+ {
+ $this->connect();
+ if ($this->_connection->inTransaction()) {
+ return true;
+ }
+
+ return $this->_connection->beginTransaction();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function commitTransaction(): bool
+ {
+ $this->connect();
+ if (!$this->_connection->inTransaction()) {
+ return false;
+ }
+
+ return $this->_connection->commit();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function rollbackTransaction(): bool
+ {
+ $this->connect();
+ if (!$this->_connection->inTransaction()) {
+ return false;
+ }
+
+ return $this->_connection->rollBack();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsSavePoints(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Returns true if the server supports common table expressions.
+ *
+ * @return bool
+ */
+ public function supportsCTEs(): bool
+ {
+ return $this->supportsCTEs === true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function quote($value, $type = PDO::PARAM_STR): string
+ {
+ $this->connect();
+
+ return $this->_connection->quote((string)$value, $type);
+ }
+
+ /**
+ * Checks if the driver supports quoting, as PDO_ODBC does not support it.
+ *
+ * @return bool
+ */
+ public function supportsQuoting(): bool
+ {
+ $this->connect();
+
+ return $this->_connection->getAttribute(PDO::ATTR_DRIVER_NAME) !== 'odbc';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ abstract public function queryTranslator(string $type): Closure;
+
+ /**
+ * @inheritDoc
+ */
+ abstract public function schemaDialect(): SchemaDialect;
+
+ /**
+ * @inheritDoc
+ */
+ abstract public function quoteIdentifier(string $identifier): string;
+
+ /**
+ * @inheritDoc
+ */
+ public function schemaValue($value): string
+ {
+ if ($value === null) {
+ return 'NULL';
+ }
+ if ($value === false) {
+ return 'FALSE';
+ }
+ if ($value === true) {
+ return 'TRUE';
+ }
+ if (is_float($value)) {
+ return str_replace(',', '.', (string)$value);
+ }
+ /** @psalm-suppress InvalidArgument */
+ if (
+ (
+ is_int($value) ||
+ $value === '0'
+ ) ||
+ (
+ is_numeric($value) &&
+ strpos($value, ',') === false &&
+ substr($value, 0, 1) !== '0' &&
+ strpos($value, 'e') === false
+ )
+ ) {
+ return (string)$value;
+ }
+
+ return $this->_connection->quote((string)$value, PDO::PARAM_STR);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function schema(): string
+ {
+ return $this->_config['schema'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function lastInsertId(?string $table = null, ?string $column = null)
+ {
+ $this->connect();
+
+ if ($this->_connection instanceof PDO) {
+ return $this->_connection->lastInsertId($table);
+ }
+
+ return $this->_connection->lastInsertId($table, $column);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isConnected(): bool
+ {
+ if ($this->_connection === null) {
+ $connected = false;
+ } else {
+ try {
+ $connected = (bool)$this->_connection->query('SELECT 1');
+ } catch (PDOException $e) {
+ $connected = false;
+ }
+ }
+
+ return $connected;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableAutoQuoting(bool $enable = true)
+ {
+ $this->_autoQuoting = $enable;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function disableAutoQuoting()
+ {
+ $this->_autoQuoting = false;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isAutoQuotingEnabled(): bool
+ {
+ return $this->_autoQuoting;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function compileQuery(Query $query, ValueBinder $binder): array
+ {
+ $processor = $this->newCompiler();
+ $translator = $this->queryTranslator($query->type());
+ $query = $translator($query);
+
+ return [$query, $processor->compile($query, $binder)];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function newCompiler(): QueryCompiler
+ {
+ return new QueryCompiler();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function newTableSchema(string $table, array $columns = []): TableSchema
+ {
+ $className = TableSchema::class;
+ if (isset($this->_config['tableSchema'])) {
+ /** @var class-string<\Cake\Database\Schema\TableSchema> $className */
+ $className = $this->_config['tableSchema'];
+ }
+
+ return new $className($table, $columns);
+ }
+
+ /**
+ * Returns the maximum alias length allowed.
+ * This can be different than the maximum identifier length for columns.
+ *
+ * @return int|null Maximum alias length or null if no limit
+ */
+ public function getMaxAliasLength(): ?int
+ {
+ return static::MAX_ALIAS_LENGTH;
+ }
+
+ /**
+ * Returns the number of connection retry attempts made.
+ *
+ * @return int
+ */
+ public function getConnectRetries(): int
+ {
+ return $this->connectRetries;
+ }
+
+ /**
+ * Destructor
+ */
+ public function __destruct()
+ {
+ /** @psalm-suppress PossiblyNullPropertyAssignmentValue */
+ $this->_connection = null;
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'connected' => $this->_connection !== null,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php
new file mode 100644
index 000000000..8afe34a85
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Mysql.php
@@ -0,0 +1,354 @@
+ true,
+ 'host' => 'localhost',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'cake',
+ 'port' => '3306',
+ 'flags' => [],
+ 'encoding' => 'utf8mb4',
+ 'timezone' => null,
+ 'init' => [],
+ ];
+
+ /**
+ * The schema dialect for this driver
+ *
+ * @var \Cake\Database\Schema\MysqlSchemaDialect|null
+ */
+ protected $_schemaDialect;
+
+ /**
+ * Whether or not the server supports native JSON
+ *
+ * @var bool|null
+ */
+ protected $_supportsNativeJson;
+
+ /**
+ * Whether or not the connected server supports window functions.
+ *
+ * @var bool|null
+ */
+ protected $_supportsWindowFunctions;
+
+ /**
+ * String used to start a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_startQuote = '`';
+
+ /**
+ * String used to end a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_endQuote = '`';
+
+ /**
+ * Server type.
+ *
+ * If the underlying server is MariaDB, its value will get set to `'mariadb'`
+ * after `version()` method is called.
+ *
+ * @var string
+ */
+ protected $serverType = self::SERVER_TYPE_MYSQL;
+
+ /**
+ * Mapping of feature to db server version for feature availability checks.
+ *
+ * @var array
+ */
+ protected $featuresToVersionMap = [
+ 'mysql' => [
+ 'json' => '5.7.0',
+ 'cte' => '8.0.0',
+ 'window' => '8.0.0',
+ ],
+ 'mariadb' => [
+ 'json' => '10.2.7',
+ 'cte' => '10.2.1',
+ 'window' => '10.2.0',
+ ],
+ ];
+
+ /**
+ * Establishes a connection to the database server
+ *
+ * @return bool true on success
+ */
+ public function connect(): bool
+ {
+ if ($this->_connection) {
+ return true;
+ }
+ $config = $this->_config;
+
+ if ($config['timezone'] === 'UTC') {
+ $config['timezone'] = '+0:00';
+ }
+
+ if (!empty($config['timezone'])) {
+ $config['init'][] = sprintf("SET time_zone = '%s'", $config['timezone']);
+ }
+
+ $config['flags'] += [
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ ];
+
+ if (!empty($config['ssl_key']) && !empty($config['ssl_cert'])) {
+ $config['flags'][PDO::MYSQL_ATTR_SSL_KEY] = $config['ssl_key'];
+ $config['flags'][PDO::MYSQL_ATTR_SSL_CERT] = $config['ssl_cert'];
+ }
+ if (!empty($config['ssl_ca'])) {
+ $config['flags'][PDO::MYSQL_ATTR_SSL_CA] = $config['ssl_ca'];
+ }
+
+ if (empty($config['unix_socket'])) {
+ $dsn = "mysql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
+ } else {
+ $dsn = "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
+ }
+
+ if (!empty($config['encoding'])) {
+ $dsn .= ";charset={$config['encoding']}";
+ }
+
+ $this->_connect($dsn, $config);
+
+ if (!empty($config['init'])) {
+ $connection = $this->getConnection();
+ foreach ((array)$config['init'] as $command) {
+ $connection->exec($command);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether php is able to use this driver for connecting to database
+ *
+ * @return bool true if it is valid to use this driver
+ */
+ public function enabled(): bool
+ {
+ return in_array('mysql', PDO::getAvailableDrivers(), true);
+ }
+
+ /**
+ * Prepares a sql statement to be executed
+ *
+ * @param string|\Cake\Database\Query $query The query to prepare.
+ * @return \Cake\Database\StatementInterface
+ */
+ public function prepare($query): StatementInterface
+ {
+ $this->connect();
+ $isObject = $query instanceof Query;
+ /**
+ * @psalm-suppress PossiblyInvalidMethodCall
+ * @psalm-suppress PossiblyInvalidArgument
+ */
+ $statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
+ $result = new MysqlStatement($statement, $this);
+ /** @psalm-suppress PossiblyInvalidMethodCall */
+ if ($isObject && $query->isBufferedResultsEnabled() === false) {
+ $result->bufferResults(false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function schemaDialect(): SchemaDialect
+ {
+ if ($this->_schemaDialect === null) {
+ $this->_schemaDialect = new MysqlSchemaDialect($this);
+ }
+
+ return $this->_schemaDialect;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function schema(): string
+ {
+ return $this->_config['database'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeySQL(): string
+ {
+ return 'SET foreign_key_checks = 0';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeySQL(): string
+ {
+ return 'SET foreign_key_checks = 1';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsDynamicConstraints(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Returns true if the connected server is MariaDB.
+ *
+ * @return bool
+ */
+ public function isMariadb(): bool
+ {
+ $this->version();
+
+ return $this->serverType === static::SERVER_TYPE_MARIADB;
+ }
+
+ /**
+ * Returns connected server version.
+ *
+ * @return string
+ */
+ public function version(): string
+ {
+ if ($this->_version === null) {
+ $this->connect();
+ $this->_version = (string)$this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
+
+ if (strpos($this->_version, 'MariaDB') !== false) {
+ $this->serverType = static::SERVER_TYPE_MARIADB;
+ preg_match('/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-MariaDB[^:]*)/', $this->_version, $matches);
+ $this->_version = $matches[1];
+ }
+ }
+
+ return $this->_version;
+ }
+
+ /**
+ * Returns true if the server supports common table expressions.
+ *
+ * @return bool
+ */
+ public function supportsCTEs(): bool
+ {
+ if ($this->supportsCTEs === null) {
+ $this->supportsCTEs = version_compare(
+ $this->version(),
+ $this->featuresToVersionMap[$this->serverType]['cte'],
+ '>='
+ );
+ }
+
+ return $this->supportsCTEs;
+ }
+
+ /**
+ * Returns true if the server supports native JSON columns
+ *
+ * @return bool
+ */
+ public function supportsNativeJson(): bool
+ {
+ if ($this->_supportsNativeJson === null) {
+ $this->_supportsNativeJson = version_compare(
+ $this->version(),
+ $this->featuresToVersionMap[$this->serverType]['json'],
+ '>='
+ );
+ }
+
+ return $this->_supportsNativeJson;
+ }
+
+ /**
+ * Returns true if the connected server supports window functions.
+ *
+ * @return bool
+ */
+ public function supportsWindowFunctions(): bool
+ {
+ if ($this->_supportsWindowFunctions === null) {
+ $this->_supportsWindowFunctions = version_compare(
+ $this->version(),
+ $this->featuresToVersionMap[$this->serverType]['window'],
+ '>='
+ );
+ }
+
+ return $this->_supportsWindowFunctions;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Postgres.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Postgres.php
new file mode 100644
index 000000000..51ca030b0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Postgres.php
@@ -0,0 +1,334 @@
+ true,
+ 'host' => 'localhost',
+ 'username' => 'root',
+ 'password' => '',
+ 'database' => 'cake',
+ 'schema' => 'public',
+ 'port' => 5432,
+ 'encoding' => 'utf8',
+ 'timezone' => null,
+ 'flags' => [],
+ 'init' => [],
+ ];
+
+ /**
+ * The schema dialect class for this driver
+ *
+ * @var \Cake\Database\Schema\PostgresSchemaDialect|null
+ */
+ protected $_schemaDialect;
+
+ /**
+ * String used to start a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_startQuote = '"';
+
+ /**
+ * String used to end a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_endQuote = '"';
+
+ /**
+ * @inheritDoc
+ */
+ protected $supportsCTEs = true;
+
+ /**
+ * Establishes a connection to the database server
+ *
+ * @return bool true on success
+ */
+ public function connect(): bool
+ {
+ if ($this->_connection) {
+ return true;
+ }
+ $config = $this->_config;
+ $config['flags'] += [
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::ATTR_EMULATE_PREPARES => false,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ ];
+ if (empty($config['unix_socket'])) {
+ $dsn = "pgsql:host={$config['host']};port={$config['port']};dbname={$config['database']}";
+ } else {
+ $dsn = "pgsql:dbname={$config['database']}";
+ }
+
+ $this->_connect($dsn, $config);
+ $this->_connection = $connection = $this->getConnection();
+ if (!empty($config['encoding'])) {
+ $this->setEncoding($config['encoding']);
+ }
+
+ if (!empty($config['schema'])) {
+ $this->setSchema($config['schema']);
+ }
+
+ if (!empty($config['timezone'])) {
+ $config['init'][] = sprintf('SET timezone = %s', $connection->quote($config['timezone']));
+ }
+
+ foreach ($config['init'] as $command) {
+ $connection->exec($command);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether php is able to use this driver for connecting to database
+ *
+ * @return bool true if it is valid to use this driver
+ */
+ public function enabled(): bool
+ {
+ return in_array('pgsql', PDO::getAvailableDrivers(), true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function schemaDialect(): SchemaDialect
+ {
+ if ($this->_schemaDialect === null) {
+ $this->_schemaDialect = new PostgresSchemaDialect($this);
+ }
+
+ return $this->_schemaDialect;
+ }
+
+ /**
+ * Sets connection encoding
+ *
+ * @param string $encoding The encoding to use.
+ * @return void
+ */
+ public function setEncoding(string $encoding): void
+ {
+ $this->connect();
+ $this->_connection->exec('SET NAMES ' . $this->_connection->quote($encoding));
+ }
+
+ /**
+ * Sets connection default schema, if any relation defined in a query is not fully qualified
+ * postgres will fallback to looking the relation into defined default schema
+ *
+ * @param string $schema The schema names to set `search_path` to.
+ * @return void
+ */
+ public function setSchema(string $schema): void
+ {
+ $this->connect();
+ $this->_connection->exec('SET search_path TO ' . $this->_connection->quote($schema));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeySQL(): string
+ {
+ return 'SET CONSTRAINTS ALL DEFERRED';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeySQL(): string
+ {
+ return 'SET CONSTRAINTS ALL IMMEDIATE';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsDynamicConstraints(): bool
+ {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _transformDistinct(Query $query): Query
+ {
+ return $query;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _insertQueryTranslator(Query $query): Query
+ {
+ if (!$query->clause('epilog')) {
+ $query->epilog('RETURNING *');
+ }
+
+ return $query;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _expressionTranslators(): array
+ {
+ return [
+ IdentifierExpression::class => '_transformIdentifierExpression',
+ FunctionExpression::class => '_transformFunctionExpression',
+ StringExpression::class => '_transformStringExpression',
+ ];
+ }
+
+ /**
+ * Changes identifer expression into postgresql format.
+ *
+ * @param \Cake\Database\Expression\IdentifierExpression $expression The expression to tranform.
+ * @return void
+ */
+ protected function _transformIdentifierExpression(IdentifierExpression $expression): void
+ {
+ $collation = $expression->getCollation();
+ if ($collation) {
+ // use trim() to work around expression being transformed multiple times
+ $expression->setCollation('"' . trim($collation, '"') . '"');
+ }
+ }
+
+ /**
+ * Receives a FunctionExpression and changes it so that it conforms to this
+ * SQL dialect.
+ *
+ * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert
+ * to postgres SQL.
+ * @return void
+ */
+ protected function _transformFunctionExpression(FunctionExpression $expression): void
+ {
+ switch ($expression->getName()) {
+ case 'CONCAT':
+ // CONCAT function is expressed as exp1 || exp2
+ $expression->setName('')->setConjunction(' ||');
+ break;
+ case 'DATEDIFF':
+ $expression
+ ->setName('')
+ ->setConjunction('-')
+ ->iterateParts(function ($p) {
+ if (is_string($p)) {
+ $p = ['value' => [$p => 'literal'], 'type' => null];
+ } else {
+ $p['value'] = [$p['value']];
+ }
+
+ return new FunctionExpression('DATE', $p['value'], [$p['type']]);
+ });
+ break;
+ case 'CURRENT_DATE':
+ $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
+ $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'date' => 'literal']);
+ break;
+ case 'CURRENT_TIME':
+ $time = new FunctionExpression('LOCALTIMESTAMP', [' 0 ' => 'literal']);
+ $expression->setName('CAST')->setConjunction(' AS ')->add([$time, 'time' => 'literal']);
+ break;
+ case 'NOW':
+ $expression->setName('LOCALTIMESTAMP')->add([' 0 ' => 'literal']);
+ break;
+ case 'RAND':
+ $expression->setName('RANDOM');
+ break;
+ case 'DATE_ADD':
+ $expression
+ ->setName('')
+ ->setConjunction(' + INTERVAL')
+ ->iterateParts(function ($p, $key) {
+ if ($key === 1) {
+ $p = sprintf("'%s'", $p);
+ }
+
+ return $p;
+ });
+ break;
+ case 'DAYOFWEEK':
+ $expression
+ ->setName('EXTRACT')
+ ->setConjunction(' ')
+ ->add(['DOW FROM' => 'literal'], [], true)
+ ->add([') + (1' => 'literal']); // Postgres starts on index 0 but Sunday should be 1
+ break;
+ }
+ }
+
+ /**
+ * Changes string expression into postgresql format.
+ *
+ * @param \Cake\Database\Expression\StringExpression $expression The string expression to tranform.
+ * @return void
+ */
+ protected function _transformStringExpression(StringExpression $expression): void
+ {
+ // use trim() to work around expression being transformed multiple times
+ $expression->setCollation('"' . trim($expression->getCollation(), '"') . '"');
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return \Cake\Database\PostgresCompiler
+ */
+ public function newCompiler(): QueryCompiler
+ {
+ return new PostgresCompiler();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/SqlDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/Driver/SqlDialectTrait.php
new file mode 100644
index 000000000..03827321d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Driver/SqlDialectTrait.php
@@ -0,0 +1,308 @@
+_startQuote . $identifier . $this->_endQuote;
+ }
+
+ // string.string
+ if (preg_match('/^[\w-]+\.[^ \*]*$/u', $identifier)) {
+ $items = explode('.', $identifier);
+
+ return $this->_startQuote . implode($this->_endQuote . '.' . $this->_startQuote, $items) . $this->_endQuote;
+ }
+
+ // string.*
+ if (preg_match('/^[\w-]+\.\*$/u', $identifier)) {
+ return $this->_startQuote . str_replace('.*', $this->_endQuote . '.*', $identifier);
+ }
+
+ // Functions
+ if (preg_match('/^([\w-]+)\((.*)\)$/', $identifier, $matches)) {
+ return $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')';
+ }
+
+ // Alias.field AS thing
+ if (preg_match('/^([\w-]+(\.[\w\s-]+|\(.*\))*)\s+AS\s*([\w-]+)$/ui', $identifier, $matches)) {
+ return $this->quoteIdentifier($matches[1]) . ' AS ' . $this->quoteIdentifier($matches[3]);
+ }
+
+ // string.string with spaces
+ if (preg_match('/^([\w-]+\.[\w][\w\s\-]*[\w])(.*)/u', $identifier, $matches)) {
+ $items = explode('.', $matches[1]);
+ $field = implode($this->_endQuote . '.' . $this->_startQuote, $items);
+
+ return $this->_startQuote . $field . $this->_endQuote . $matches[2];
+ }
+
+ if (preg_match('/^[\w_\s-]*[\w_-]+/u', $identifier)) {
+ return $this->_startQuote . $identifier . $this->_endQuote;
+ }
+
+ return $identifier;
+ }
+
+ /**
+ * Returns a callable function that will be used to transform a passed Query object.
+ * This function, in turn, will return an instance of a Query object that has been
+ * transformed to accommodate any specificities of the SQL dialect in use.
+ *
+ * @param string $type the type of query to be transformed
+ * (select, insert, update, delete)
+ * @return \Closure
+ */
+ public function queryTranslator(string $type): Closure
+ {
+ return function ($query) use ($type) {
+ if ($this->isAutoQuotingEnabled()) {
+ $query = (new IdentifierQuoter($this))->quote($query);
+ }
+
+ /** @var \Cake\ORM\Query $query */
+ $query = $this->{'_' . $type . 'QueryTranslator'}($query);
+ $translators = $this->_expressionTranslators();
+ if (!$translators) {
+ return $query;
+ }
+
+ $query->traverseExpressions(function ($expression) use ($translators, $query): void {
+ foreach ($translators as $class => $method) {
+ if ($expression instanceof $class) {
+ $this->{$method}($expression, $query);
+ }
+ }
+ });
+
+ return $query;
+ };
+ }
+
+ /**
+ * Returns an associative array of methods that will transform Expression
+ * objects to conform with the specific SQL dialect. Keys are class names
+ * and values a method in this class.
+ *
+ * @psalm-return array
+ * @return string[]
+ */
+ protected function _expressionTranslators(): array
+ {
+ return [];
+ }
+
+ /**
+ * Apply translation steps to select queries.
+ *
+ * @param \Cake\Database\Query $query The query to translate
+ * @return \Cake\Database\Query The modified query
+ */
+ protected function _selectQueryTranslator(Query $query): Query
+ {
+ return $this->_transformDistinct($query);
+ }
+
+ /**
+ * Returns the passed query after rewriting the DISTINCT clause, so that drivers
+ * that do not support the "ON" part can provide the actual way it should be done
+ *
+ * @param \Cake\Database\Query $query The query to be transformed
+ * @return \Cake\Database\Query
+ */
+ protected function _transformDistinct(Query $query): Query
+ {
+ if (is_array($query->clause('distinct'))) {
+ $query->group($query->clause('distinct'), true);
+ $query->distinct(false);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Apply translation steps to delete queries.
+ *
+ * Chops out aliases on delete query conditions as most database dialects do not
+ * support aliases in delete queries. This also removes aliases
+ * in table names as they frequently don't work either.
+ *
+ * We are intentionally not supporting deletes with joins as they have even poorer support.
+ *
+ * @param \Cake\Database\Query $query The query to translate
+ * @return \Cake\Database\Query The modified query
+ */
+ protected function _deleteQueryTranslator(Query $query): Query
+ {
+ $hadAlias = false;
+ $tables = [];
+ foreach ($query->clause('from') as $alias => $table) {
+ if (is_string($alias)) {
+ $hadAlias = true;
+ }
+ $tables[] = $table;
+ }
+ if ($hadAlias) {
+ $query->from($tables, true);
+ }
+
+ if (!$hadAlias) {
+ return $query;
+ }
+
+ return $this->_removeAliasesFromConditions($query);
+ }
+
+ /**
+ * Apply translation steps to update queries.
+ *
+ * Chops out aliases on update query conditions as not all database dialects do support
+ * aliases in update queries.
+ *
+ * Just like for delete queries, joins are currently not supported for update queries.
+ *
+ * @param \Cake\Database\Query $query The query to translate
+ * @return \Cake\Database\Query The modified query
+ */
+ protected function _updateQueryTranslator(Query $query): Query
+ {
+ return $this->_removeAliasesFromConditions($query);
+ }
+
+ /**
+ * Removes aliases from the `WHERE` clause of a query.
+ *
+ * @param \Cake\Database\Query $query The query to process.
+ * @return \Cake\Database\Query The modified query.
+ * @throws \RuntimeException In case the processed query contains any joins, as removing
+ * aliases from the conditions can break references to the joined tables.
+ */
+ protected function _removeAliasesFromConditions(Query $query): Query
+ {
+ if ($query->clause('join')) {
+ throw new RuntimeException(
+ 'Aliases are being removed from conditions for UPDATE/DELETE queries, ' .
+ 'this can break references to joined tables.'
+ );
+ }
+
+ $conditions = $query->clause('where');
+ if ($conditions) {
+ $conditions->traverse(function ($expression) {
+ if ($expression instanceof ComparisonExpression) {
+ $field = $expression->getField();
+ if (
+ is_string($field) &&
+ strpos($field, '.') !== false
+ ) {
+ [, $unaliasedField] = explode('.', $field, 2);
+ $expression->setField($unaliasedField);
+ }
+
+ return $expression;
+ }
+
+ if ($expression instanceof IdentifierExpression) {
+ $identifier = $expression->getIdentifier();
+ if (strpos($identifier, '.') !== false) {
+ [, $unaliasedIdentifier] = explode('.', $identifier, 2);
+ $expression->setIdentifier($unaliasedIdentifier);
+ }
+
+ return $expression;
+ }
+
+ return $expression;
+ });
+ }
+
+ return $query;
+ }
+
+ /**
+ * Apply translation steps to insert queries.
+ *
+ * @param \Cake\Database\Query $query The query to translate
+ * @return \Cake\Database\Query The modified query
+ */
+ protected function _insertQueryTranslator(Query $query): Query
+ {
+ return $query;
+ }
+
+ /**
+ * Returns a SQL snippet for creating a new transaction savepoint
+ *
+ * @param string|int $name save point name
+ * @return string
+ */
+ public function savePointSQL($name): string
+ {
+ return 'SAVEPOINT LEVEL' . $name;
+ }
+
+ /**
+ * Returns a SQL snippet for releasing a previously created save point
+ *
+ * @param string|int $name save point name
+ * @return string
+ */
+ public function releaseSavePointSQL($name): string
+ {
+ return 'RELEASE SAVEPOINT LEVEL' . $name;
+ }
+
+ /**
+ * Returns a SQL snippet for rollbacking a previously created save point
+ *
+ * @param string|int $name save point name
+ * @return string
+ */
+ public function rollbackSavePointSQL($name): string
+ {
+ return 'ROLLBACK TO SAVEPOINT LEVEL' . $name;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlite.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlite.php
new file mode 100644
index 000000000..d81748e54
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlite.php
@@ -0,0 +1,334 @@
+ false,
+ 'username' => null,
+ 'password' => null,
+ 'database' => ':memory:',
+ 'encoding' => 'utf8',
+ 'mask' => 0644,
+ 'flags' => [],
+ 'init' => [],
+ ];
+
+ /**
+ * The schema dialect class for this driver
+ *
+ * @var \Cake\Database\Schema\SqliteSchemaDialect|null
+ */
+ protected $_schemaDialect;
+
+ /**
+ * Whether or not the connected server supports window functions.
+ *
+ * @var bool|null
+ */
+ protected $_supportsWindowFunctions;
+
+ /**
+ * String used to start a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_startQuote = '"';
+
+ /**
+ * String used to end a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_endQuote = '"';
+
+ /**
+ * Mapping of date parts.
+ *
+ * @var array
+ */
+ protected $_dateParts = [
+ 'day' => 'd',
+ 'hour' => 'H',
+ 'month' => 'm',
+ 'minute' => 'M',
+ 'second' => 'S',
+ 'week' => 'W',
+ 'year' => 'Y',
+ ];
+
+ /**
+ * Establishes a connection to the database server
+ *
+ * @return bool true on success
+ */
+ public function connect(): bool
+ {
+ if ($this->_connection) {
+ return true;
+ }
+ $config = $this->_config;
+ $config['flags'] += [
+ PDO::ATTR_PERSISTENT => $config['persistent'],
+ PDO::ATTR_EMULATE_PREPARES => false,
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ ];
+ if (!is_string($config['database']) || !strlen($config['database'])) {
+ $name = $config['name'] ?? 'unknown';
+ throw new InvalidArgumentException(
+ "The `database` key for the `{$name}` SQLite connection needs to be a non-empty string."
+ );
+ }
+
+ $databaseExists = file_exists($config['database']);
+
+ $dsn = "sqlite:{$config['database']}";
+ $this->_connect($dsn, $config);
+
+ if (!$databaseExists && $config['database'] !== ':memory:') {
+ // phpcs:disable
+ @chmod($config['database'], $config['mask']);
+ // phpcs:enable
+ }
+
+ if (!empty($config['init'])) {
+ foreach ((array)$config['init'] as $command) {
+ $this->getConnection()->exec($command);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether php is able to use this driver for connecting to database
+ *
+ * @return bool true if it is valid to use this driver
+ */
+ public function enabled(): bool
+ {
+ return in_array('sqlite', PDO::getAvailableDrivers(), true);
+ }
+
+ /**
+ * Prepares a sql statement to be executed
+ *
+ * @param string|\Cake\Database\Query $query The query to prepare.
+ * @return \Cake\Database\StatementInterface
+ */
+ public function prepare($query): StatementInterface
+ {
+ $this->connect();
+ $isObject = $query instanceof Query;
+ /**
+ * @psalm-suppress PossiblyInvalidMethodCall
+ * @psalm-suppress PossiblyInvalidArgument
+ */
+ $statement = $this->_connection->prepare($isObject ? $query->sql() : $query);
+ $result = new SqliteStatement(new PDOStatement($statement, $this), $this);
+ /** @psalm-suppress PossiblyInvalidMethodCall */
+ if ($isObject && $query->isBufferedResultsEnabled() === false) {
+ $result->bufferResults(false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeySQL(): string
+ {
+ return 'PRAGMA foreign_keys = OFF';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeySQL(): string
+ {
+ return 'PRAGMA foreign_keys = ON';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsDynamicConstraints(): bool
+ {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function schemaDialect(): SchemaDialect
+ {
+ if ($this->_schemaDialect === null) {
+ $this->_schemaDialect = new SqliteSchemaDialect($this);
+ }
+
+ return $this->_schemaDialect;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function newCompiler(): QueryCompiler
+ {
+ return new SqliteCompiler();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _expressionTranslators(): array
+ {
+ return [
+ FunctionExpression::class => '_transformFunctionExpression',
+ TupleComparison::class => '_transformTupleComparison',
+ ];
+ }
+
+ /**
+ * Receives a FunctionExpression and changes it so that it conforms to this
+ * SQL dialect.
+ *
+ * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
+ * @return void
+ */
+ protected function _transformFunctionExpression(FunctionExpression $expression): void
+ {
+ switch ($expression->getName()) {
+ case 'CONCAT':
+ // CONCAT function is expressed as exp1 || exp2
+ $expression->setName('')->setConjunction(' ||');
+ break;
+ case 'DATEDIFF':
+ $expression
+ ->setName('ROUND')
+ ->setConjunction('-')
+ ->iterateParts(function ($p) {
+ return new FunctionExpression('JULIANDAY', [$p['value']], [$p['type']]);
+ });
+ break;
+ case 'NOW':
+ $expression->setName('DATETIME')->add(["'now'" => 'literal']);
+ break;
+ case 'RAND':
+ $expression
+ ->setName('ABS')
+ ->add(['RANDOM() % 1' => 'literal'], [], true);
+ break;
+ case 'CURRENT_DATE':
+ $expression->setName('DATE')->add(["'now'" => 'literal']);
+ break;
+ case 'CURRENT_TIME':
+ $expression->setName('TIME')->add(["'now'" => 'literal']);
+ break;
+ case 'EXTRACT':
+ $expression
+ ->setName('STRFTIME')
+ ->setConjunction(' ,')
+ ->iterateParts(function ($p, $key) {
+ if ($key === 0) {
+ $value = rtrim(strtolower($p), 's');
+ if (isset($this->_dateParts[$value])) {
+ $p = ['value' => '%' . $this->_dateParts[$value], 'type' => null];
+ }
+ }
+
+ return $p;
+ });
+ break;
+ case 'DATE_ADD':
+ $expression
+ ->setName('DATE')
+ ->setConjunction(',')
+ ->iterateParts(function ($p, $key) {
+ if ($key === 1) {
+ $p = ['value' => $p, 'type' => null];
+ }
+
+ return $p;
+ });
+ break;
+ case 'DAYOFWEEK':
+ $expression
+ ->setName('STRFTIME')
+ ->setConjunction(' ')
+ ->add(["'%w', " => 'literal'], [], true)
+ ->add([') + (1' => 'literal']); // Sqlite starts on index 0 but Sunday should be 1
+ break;
+ }
+ }
+
+ /**
+ * Returns true if the server supports common table expressions.
+ *
+ * @return bool
+ */
+ public function supportsCTEs(): bool
+ {
+ if ($this->supportsCTEs === null) {
+ $this->supportsCTEs = version_compare($this->version(), '3.8.3', '>=');
+ }
+
+ return $this->supportsCTEs;
+ }
+
+ /**
+ * Returns true if the connected server supports window functions.
+ *
+ * @return bool
+ */
+ public function supportsWindowFunctions(): bool
+ {
+ if ($this->_supportsWindowFunctions === null) {
+ $this->_supportsWindowFunctions = version_compare($this->version(), '3.25.0', '>=');
+ }
+
+ return $this->_supportsWindowFunctions;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlserver.php b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlserver.php
new file mode 100644
index 000000000..3a6555373
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Driver/Sqlserver.php
@@ -0,0 +1,545 @@
+ 'localhost\SQLEXPRESS',
+ 'username' => '',
+ 'password' => '',
+ 'database' => 'cake',
+ 'port' => '',
+ // PDO::SQLSRV_ENCODING_UTF8
+ 'encoding' => 65001,
+ 'flags' => [],
+ 'init' => [],
+ 'settings' => [],
+ 'attributes' => [],
+ 'app' => null,
+ 'connectionPooling' => null,
+ 'failoverPartner' => null,
+ 'loginTimeout' => null,
+ 'multiSubnetFailover' => null,
+ ];
+
+ /**
+ * The schema dialect class for this driver
+ *
+ * @var \Cake\Database\Schema\SqlserverSchemaDialect|null
+ */
+ protected $_schemaDialect;
+
+ /**
+ * String used to start a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_startQuote = '[';
+
+ /**
+ * String used to end a database identifier quoting to make it safe
+ *
+ * @var string
+ */
+ protected $_endQuote = ']';
+
+ /**
+ * @inheritDoc
+ */
+ protected $supportsCTEs = true;
+
+ /**
+ * Establishes a connection to the database server.
+ *
+ * Please note that the PDO::ATTR_PERSISTENT attribute is not supported by
+ * the SQL Server PHP PDO drivers. As a result you cannot use the
+ * persistent config option when connecting to a SQL Server (for more
+ * information see: https://github.com/Microsoft/msphpsql/issues/65).
+ *
+ * @throws \InvalidArgumentException if an unsupported setting is in the driver config
+ * @return bool true on success
+ */
+ public function connect(): bool
+ {
+ if ($this->_connection) {
+ return true;
+ }
+ $config = $this->_config;
+
+ if (isset($config['persistent']) && $config['persistent']) {
+ throw new InvalidArgumentException(
+ 'Config setting "persistent" cannot be set to true, '
+ . 'as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT'
+ );
+ }
+
+ $config['flags'] += [
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+ ];
+
+ if (!empty($config['encoding'])) {
+ $config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
+ }
+ $port = '';
+ if ($config['port']) {
+ $port = ',' . $config['port'];
+ }
+
+ $dsn = "sqlsrv:Server={$config['host']}{$port};Database={$config['database']};MultipleActiveResultSets=false";
+ if ($config['app'] !== null) {
+ $dsn .= ";APP={$config['app']}";
+ }
+ if ($config['connectionPooling'] !== null) {
+ $dsn .= ";ConnectionPooling={$config['connectionPooling']}";
+ }
+ if ($config['failoverPartner'] !== null) {
+ $dsn .= ";Failover_Partner={$config['failoverPartner']}";
+ }
+ if ($config['loginTimeout'] !== null) {
+ $dsn .= ";LoginTimeout={$config['loginTimeout']}";
+ }
+ if ($config['multiSubnetFailover'] !== null) {
+ $dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
+ }
+ $this->_connect($dsn, $config);
+
+ $connection = $this->getConnection();
+ if (!empty($config['init'])) {
+ foreach ((array)$config['init'] as $command) {
+ $connection->exec($command);
+ }
+ }
+ if (!empty($config['settings']) && is_array($config['settings'])) {
+ foreach ($config['settings'] as $key => $value) {
+ $connection->exec("SET {$key} {$value}");
+ }
+ }
+ if (!empty($config['attributes']) && is_array($config['attributes'])) {
+ foreach ($config['attributes'] as $key => $value) {
+ $connection->setAttribute($key, $value);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether PHP is able to use this driver for connecting to database
+ *
+ * @return bool true if it is valid to use this driver
+ */
+ public function enabled(): bool
+ {
+ return in_array('sqlsrv', PDO::getAvailableDrivers(), true);
+ }
+
+ /**
+ * Prepares a sql statement to be executed
+ *
+ * @param string|\Cake\Database\Query $query The query to prepare.
+ * @return \Cake\Database\StatementInterface
+ */
+ public function prepare($query): StatementInterface
+ {
+ $this->connect();
+
+ $sql = $query;
+ $options = [
+ PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL,
+ PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,
+ ];
+ if ($query instanceof Query) {
+ $sql = $query->sql();
+ if (count($query->getValueBinder()->bindings()) > 2100) {
+ throw new InvalidArgumentException(
+ 'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server. ' .
+ 'This is probably due to a very large WHERE IN () clause which generates a parameter ' .
+ 'for each value in the array. ' .
+ 'If using an Association, try changing the `strategy` from select to subquery.'
+ );
+ }
+
+ if (!$query->isBufferedResultsEnabled()) {
+ $options = [];
+ }
+ }
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $statement = $this->_connection->prepare($sql, $options);
+
+ return new SqlserverStatement($statement, $this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function savePointSQL($name): string
+ {
+ return 'SAVE TRANSACTION t' . $name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function releaseSavePointSQL($name): string
+ {
+ return 'COMMIT TRANSACTION t' . $name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function rollbackSavePointSQL($name): string
+ {
+ return 'ROLLBACK TRANSACTION t' . $name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function disableForeignKeySQL(): string
+ {
+ return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableForeignKeySQL(): string
+ {
+ return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsDynamicConstraints(): bool
+ {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function schemaDialect(): SchemaDialect
+ {
+ if ($this->_schemaDialect === null) {
+ $this->_schemaDialect = new SqlserverSchemaDialect($this);
+ }
+
+ return $this->_schemaDialect;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return \Cake\Database\SqlserverCompiler
+ */
+ public function newCompiler(): QueryCompiler
+ {
+ return new SqlserverCompiler();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _selectQueryTranslator(Query $query): Query
+ {
+ $limit = $query->clause('limit');
+ $offset = $query->clause('offset');
+
+ if ($limit && $offset === null) {
+ $query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
+ }
+
+ if ($offset !== null && !$query->clause('order')) {
+ $query->order($query->newExpr()->add('(SELECT NULL)'));
+ }
+
+ if ($this->version() < 11 && $offset !== null) {
+ return $this->_pagingSubquery($query, $limit, $offset);
+ }
+
+ return $this->_transformDistinct($query);
+ }
+
+ /**
+ * Generate a paging subquery for older versions of SQLserver.
+ *
+ * Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
+ * be used.
+ *
+ * @param \Cake\Database\Query $original The query to wrap in a subquery.
+ * @param int|null $limit The number of rows to fetch.
+ * @param int|null $offset The number of rows to offset.
+ * @return \Cake\Database\Query Modified query object.
+ */
+ protected function _pagingSubquery(Query $original, ?int $limit, ?int $offset): Query
+ {
+ $field = '_cake_paging_._cake_page_rownum_';
+
+ if ($original->clause('order')) {
+ // SQL server does not support column aliases in OVER clauses. But
+ // the only practical way to specify the use of calculated columns
+ // is with their alias. So substitute the select SQL in place of
+ // any column aliases for those entries in the order clause.
+ $select = $original->clause('select');
+ $order = new OrderByExpression();
+ $original
+ ->clause('order')
+ ->iterateParts(function ($direction, $orderBy) use ($select, $order) {
+ $key = $orderBy;
+ if (
+ isset($select[$orderBy]) &&
+ $select[$orderBy] instanceof ExpressionInterface
+ ) {
+ $order->add(new OrderClauseExpression($select[$orderBy], $direction));
+ } else {
+ $order->add([$key => $direction]);
+ }
+
+ // Leave original order clause unchanged.
+ return $orderBy;
+ });
+ } else {
+ $order = new OrderByExpression('(SELECT NULL)');
+ }
+
+ $query = clone $original;
+ $query->select([
+ '_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order),
+ ])->limit(null)
+ ->offset(null)
+ ->order([], true);
+
+ $outer = new Query($query->getConnection());
+ $outer->select('*')
+ ->from(['_cake_paging_' => $query]);
+
+ if ($offset) {
+ $outer->where(["$field > " . $offset]);
+ }
+ if ($limit) {
+ $value = (int)$offset + $limit;
+ $outer->where(["$field <= $value"]);
+ }
+
+ // Decorate the original query as that is what the
+ // end developer will be calling execute() on originally.
+ $original->decorateResults(function ($row) {
+ if (isset($row['_cake_page_rownum_'])) {
+ unset($row['_cake_page_rownum_']);
+ }
+
+ return $row;
+ });
+
+ return $outer;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _transformDistinct(Query $query): Query
+ {
+ if (!is_array($query->clause('distinct'))) {
+ return $query;
+ }
+
+ $original = $query;
+ $query = clone $original;
+
+ $distinct = $query->clause('distinct');
+ $query->distinct(false);
+
+ $order = new OrderByExpression($distinct);
+ $query
+ ->select(function ($q) use ($distinct, $order) {
+ $over = $q->newExpr('ROW_NUMBER() OVER')
+ ->add('(PARTITION BY')
+ ->add($q->newExpr()->add($distinct)->setConjunction(','))
+ ->add($order)
+ ->add(')')
+ ->setConjunction(' ');
+
+ return [
+ '_cake_distinct_pivot_' => $over,
+ ];
+ })
+ ->limit(null)
+ ->offset(null)
+ ->order([], true);
+
+ $outer = new Query($query->getConnection());
+ $outer->select('*')
+ ->from(['_cake_distinct_' => $query])
+ ->where(['_cake_distinct_pivot_' => 1]);
+
+ // Decorate the original query as that is what the
+ // end developer will be calling execute() on originally.
+ $original->decorateResults(function ($row) {
+ if (isset($row['_cake_distinct_pivot_'])) {
+ unset($row['_cake_distinct_pivot_']);
+ }
+
+ return $row;
+ });
+
+ return $outer;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _expressionTranslators(): array
+ {
+ return [
+ FunctionExpression::class => '_transformFunctionExpression',
+ TupleComparison::class => '_transformTupleComparison',
+ ];
+ }
+
+ /**
+ * Receives a FunctionExpression and changes it so that it conforms to this
+ * SQL dialect.
+ *
+ * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
+ * @return void
+ */
+ protected function _transformFunctionExpression(FunctionExpression $expression): void
+ {
+ switch ($expression->getName()) {
+ case 'CONCAT':
+ // CONCAT function is expressed as exp1 + exp2
+ $expression->setName('')->setConjunction(' +');
+ break;
+ case 'DATEDIFF':
+ /** @var bool $hasDay */
+ $hasDay = false;
+ $visitor = function ($value) use (&$hasDay) {
+ if ($value === 'day') {
+ $hasDay = true;
+ }
+
+ return $value;
+ };
+ $expression->iterateParts($visitor);
+
+ if (!$hasDay) {
+ $expression->add(['day' => 'literal'], [], true);
+ }
+ break;
+ case 'CURRENT_DATE':
+ $time = new FunctionExpression('GETUTCDATE');
+ $expression->setName('CONVERT')->add(['date' => 'literal', $time]);
+ break;
+ case 'CURRENT_TIME':
+ $time = new FunctionExpression('GETUTCDATE');
+ $expression->setName('CONVERT')->add(['time' => 'literal', $time]);
+ break;
+ case 'NOW':
+ $expression->setName('GETUTCDATE');
+ break;
+ case 'EXTRACT':
+ $expression->setName('DATEPART')->setConjunction(' ,');
+ break;
+ case 'DATE_ADD':
+ $params = [];
+ $visitor = function ($p, $key) use (&$params) {
+ if ($key === 0) {
+ $params[2] = $p;
+ } else {
+ $valueUnit = explode(' ', $p);
+ $params[0] = rtrim($valueUnit[1], 's');
+ $params[1] = $valueUnit[0];
+ }
+
+ return $p;
+ };
+ $manipulator = function ($p, $key) use (&$params) {
+ return $params[$key];
+ };
+
+ $expression
+ ->setName('DATEADD')
+ ->setConjunction(',')
+ ->iterateParts($visitor)
+ ->iterateParts($manipulator)
+ ->add([$params[2] => 'literal']);
+ break;
+ case 'DAYOFWEEK':
+ $expression
+ ->setName('DATEPART')
+ ->setConjunction(' ')
+ ->add(['weekday, ' => 'literal'], [], true);
+ break;
+ case 'SUBSTR':
+ $expression->setName('SUBSTRING');
+ if (count($expression) < 4) {
+ $params = [];
+ $expression
+ ->iterateParts(function ($p) use (&$params) {
+ return $params[] = $p;
+ })
+ ->add([new FunctionExpression('LEN', [$params[0]]), ['string']]);
+ }
+
+ break;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Driver/TupleComparisonTranslatorTrait.php b/app/vendor/cakephp/cakephp/src/Database/Driver/TupleComparisonTranslatorTrait.php
new file mode 100644
index 000000000..c3cf72fc0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Driver/TupleComparisonTranslatorTrait.php
@@ -0,0 +1,103 @@
+getField();
+
+ if (!is_array($fields)) {
+ return;
+ }
+
+ $value = $expression->getValue();
+ $op = $expression->getOperator();
+ $true = new QueryExpression('1');
+
+ if ($value instanceof Query) {
+ $selected = array_values($value->clause('select'));
+ foreach ($fields as $i => $field) {
+ $value->andWhere([$field . " $op" => new IdentifierExpression($selected[$i])]);
+ }
+ $value->select($true, true);
+ $expression->setField($true);
+ $expression->setOperator('=');
+
+ return;
+ }
+
+ $type = $expression->getType();
+ if ($type) {
+ $typeMap = array_combine($fields, $type);
+ } else {
+ $typeMap = [];
+ }
+
+ $surrogate = $query->getConnection()
+ ->newQuery()
+ ->select($true);
+
+ if (!is_array(current($value))) {
+ $value = [$value];
+ }
+
+ $conditions = ['OR' => []];
+ foreach ($value as $tuple) {
+ $item = [];
+ foreach (array_values($tuple) as $i => $value2) {
+ $item[] = [$fields[$i] => $value2];
+ }
+ $conditions['OR'][] = $item;
+ }
+ $surrogate->where($conditions, $typeMap);
+
+ $expression->setField($true);
+ $expression->setValue($surrogate);
+ $expression->setOperator('=');
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/DriverInterface.php b/app/vendor/cakephp/cakephp/src/Database/DriverInterface.php
new file mode 100644
index 000000000..5587b795f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/DriverInterface.php
@@ -0,0 +1,280 @@
+filter === null) {
+ $this->filter = new QueryExpression();
+ }
+
+ if ($conditions instanceof Closure) {
+ $conditions = $conditions(new QueryExpression());
+ }
+
+ $this->filter->add($conditions, $types);
+
+ return $this;
+ }
+
+ /**
+ * Adds an empty `OVER()` window expression or a named window epression.
+ *
+ * @param string|null $name Window name
+ * @return $this
+ */
+ public function over(?string $name = null)
+ {
+ if ($this->window === null) {
+ $this->window = new WindowExpression();
+ }
+ if ($name) {
+ // Set name manually in case this was chained from FunctionsBuilder wrapper
+ $this->window->name($name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function partition($partitions)
+ {
+ $this->over();
+ $this->window->partition($partitions);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function order($fields)
+ {
+ $this->over();
+ $this->window->order($fields);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function range($start, $end = 0)
+ {
+ $this->over();
+ $this->window->range($start, $end);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function rows(?int $start, ?int $end = 0)
+ {
+ $this->over();
+ $this->window->rows($start, $end);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function groups(?int $start, ?int $end = 0)
+ {
+ $this->over();
+ $this->window->groups($start, $end);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function frame(
+ string $type,
+ $startOffset,
+ string $startDirection,
+ $endOffset,
+ string $endDirection
+ ) {
+ $this->over();
+ $this->window->frame($type, $startOffset, $startDirection, $endOffset, $endDirection);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function excludeCurrent()
+ {
+ $this->over();
+ $this->window->excludeCurrent();
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function excludeGroup()
+ {
+ $this->over();
+ $this->window->excludeGroup();
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function excludeTies()
+ {
+ $this->over();
+ $this->window->excludeTies();
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $sql = parent::sql($binder);
+ if ($this->filter !== null) {
+ $sql .= ' FILTER (WHERE ' . $this->filter->sql($binder) . ')';
+ }
+ if ($this->window !== null) {
+ if ($this->window->isNamedOnly()) {
+ $sql .= ' OVER ' . $this->window->sql($binder);
+ } else {
+ $sql .= ' OVER (' . $this->window->sql($binder) . ')';
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ parent::traverse($callback);
+ if ($this->filter !== null) {
+ $callback($this->filter);
+ $this->filter->traverse($callback);
+ }
+ if ($this->window !== null) {
+ $callback($this->window);
+ $this->window->traverse($callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function count(): int
+ {
+ $count = parent::count();
+ if ($this->window !== null) {
+ $count = $count + 1;
+ }
+
+ return $count;
+ }
+
+ /**
+ * Clone this object and its subtree of expressions.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ parent::__clone();
+ if ($this->filter !== null) {
+ $this->filter = clone $this->filter;
+ }
+ if ($this->window !== null) {
+ $this->window = clone $this->window;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/BetweenExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/BetweenExpression.php
new file mode 100644
index 000000000..34b7a1ee6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/BetweenExpression.php
@@ -0,0 +1,144 @@
+_castToExpression($from, $type);
+ $to = $this->_castToExpression($to, $type);
+ }
+
+ $this->_field = $field;
+ $this->_from = $from;
+ $this->_to = $to;
+ $this->_type = $type;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $parts = [
+ 'from' => $this->_from,
+ 'to' => $this->_to,
+ ];
+
+ /** @var string|\Cake\Database\ExpressionInterface $field */
+ $field = $this->_field;
+ if ($field instanceof ExpressionInterface) {
+ $field = $field->sql($binder);
+ }
+
+ foreach ($parts as $name => $part) {
+ if ($part instanceof ExpressionInterface) {
+ $parts[$name] = $part->sql($binder);
+ continue;
+ }
+ $parts[$name] = $this->_bindValue($part, $binder, $this->_type);
+ }
+
+ return sprintf('%s BETWEEN %s AND %s', $field, $parts['from'], $parts['to']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ foreach ([$this->_field, $this->_from, $this->_to] as $part) {
+ if ($part instanceof ExpressionInterface) {
+ $callback($part);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Registers a value in the placeholder generator and returns the generated placeholder
+ *
+ * @param mixed $value The value to bind
+ * @param \Cake\Database\ValueBinder $binder The value binder to use
+ * @param string $type The type of $value
+ * @return string generated placeholder
+ */
+ protected function _bindValue($value, $binder, $type): string
+ {
+ $placeholder = $binder->placeholder('c');
+ $binder->bind($placeholder, $value, $type);
+
+ return $placeholder;
+ }
+
+ /**
+ * Do a deep clone of this expression.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ foreach (['_field', '_from', '_to'] as $part) {
+ if ($this->{$part} instanceof ExpressionInterface) {
+ $this->{$part} = clone $this->{$part};
+ }
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/CaseExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/CaseExpression.php
new file mode 100644
index 000000000..d8c6ebe13
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/CaseExpression.php
@@ -0,0 +1,249 @@
+ :value"
+ *
+ * @var array
+ */
+ protected $_conditions = [];
+
+ /**
+ * Values that are associated with the conditions in the $_conditions array.
+ * Each value represents the 'true' value for the condition with the corresponding key.
+ *
+ * @var array
+ */
+ protected $_values = [];
+
+ /**
+ * The `ELSE` value for the case statement. If null then no `ELSE` will be included.
+ *
+ * @var string|\Cake\Database\ExpressionInterface|array|null
+ */
+ protected $_elseValue;
+
+ /**
+ * Constructs the case expression
+ *
+ * @param array|\Cake\Database\ExpressionInterface $conditions The conditions to test. Must be a ExpressionInterface
+ * instance, or an array of ExpressionInterface instances.
+ * @param array|\Cake\Database\ExpressionInterface $values associative array of values to be associated with the
+ * conditions passed in $conditions. If there are more $values than $conditions,
+ * the last $value is used as the `ELSE` value.
+ * @param array $types associative array of types to be associated with the values
+ * passed in $values
+ */
+ public function __construct($conditions = [], $values = [], $types = [])
+ {
+ if (!empty($conditions)) {
+ $this->add($conditions, $values, $types);
+ }
+
+ if (is_array($conditions) && is_array($values) && count($values) > count($conditions)) {
+ end($values);
+ $key = key($values);
+ $this->elseValue($values[$key], $types[$key] ?? null);
+ }
+ }
+
+ /**
+ * Adds one or more conditions and their respective true values to the case object.
+ * Conditions must be a one dimensional array or a QueryExpression.
+ * The trueValues must be a similar structure, but may contain a string value.
+ *
+ * @param array|\Cake\Database\ExpressionInterface $conditions Must be a ExpressionInterface instance,
+ * or an array of ExpressionInterface instances.
+ * @param array|\Cake\Database\ExpressionInterface $values associative array of values of each condition
+ * @param array $types associative array of types to be associated with the values
+ * @return $this
+ */
+ public function add($conditions = [], $values = [], $types = [])
+ {
+ if (!is_array($conditions)) {
+ $conditions = [$conditions];
+ }
+ if (!is_array($values)) {
+ $values = [$values];
+ }
+ if (!is_array($types)) {
+ $types = [$types];
+ }
+
+ $this->_addExpressions($conditions, $values, $types);
+
+ return $this;
+ }
+
+ /**
+ * Iterates over the passed in conditions and ensures that there is a matching true value for each.
+ * If no matching true value, then it is defaulted to '1'.
+ *
+ * @param array $conditions Array of ExpressionInterface instances.
+ * @param array $values associative array of values of each condition
+ * @param array $types associative array of types to be associated with the values
+ * @return void
+ */
+ protected function _addExpressions(array $conditions, array $values, array $types): void
+ {
+ $rawValues = array_values($values);
+ $keyValues = array_keys($values);
+
+ foreach ($conditions as $k => $c) {
+ $numericKey = is_numeric($k);
+
+ if ($numericKey && empty($c)) {
+ continue;
+ }
+
+ if (!$c instanceof ExpressionInterface) {
+ continue;
+ }
+
+ $this->_conditions[] = $c;
+ $value = $rawValues[$k] ?? 1;
+
+ if ($value === 'literal') {
+ $value = $keyValues[$k];
+ $this->_values[] = $value;
+ continue;
+ }
+
+ if ($value === 'identifier') {
+ $value = new IdentifierExpression($keyValues[$k]);
+ $this->_values[] = $value;
+ continue;
+ }
+
+ $type = $types[$k] ?? null;
+
+ if ($type !== null && !$value instanceof ExpressionInterface) {
+ $value = $this->_castToExpression($value, $type);
+ }
+
+ if ($value instanceof ExpressionInterface) {
+ $this->_values[] = $value;
+ continue;
+ }
+
+ $this->_values[] = ['value' => $value, 'type' => $type];
+ }
+ }
+
+ /**
+ * Sets the default value
+ *
+ * @param \Cake\Database\ExpressionInterface|string|array|null $value Value to set
+ * @param string|null $type Type of value
+ * @return void
+ */
+ public function elseValue($value = null, ?string $type = null): void
+ {
+ if (is_array($value)) {
+ end($value);
+ $value = key($value);
+ }
+
+ if ($value !== null && !$value instanceof ExpressionInterface) {
+ $value = $this->_castToExpression($value, $type);
+ }
+
+ if (!$value instanceof ExpressionInterface) {
+ $value = ['value' => $value, 'type' => $type];
+ }
+
+ $this->_elseValue = $value;
+ }
+
+ /**
+ * Compiles the relevant parts into sql
+ *
+ * @param array|string|\Cake\Database\ExpressionInterface $part The part to compile
+ * @param \Cake\Database\ValueBinder $binder Sql generator
+ * @return string
+ */
+ protected function _compile($part, ValueBinder $binder): string
+ {
+ if ($part instanceof ExpressionInterface) {
+ $part = $part->sql($binder);
+ } elseif (is_array($part)) {
+ $placeholder = $binder->placeholder('param');
+ $binder->bind($placeholder, $part['value'], $part['type']);
+ $part = $placeholder;
+ }
+
+ return $part;
+ }
+
+ /**
+ * Converts the Node into a SQL string fragment.
+ *
+ * @param \Cake\Database\ValueBinder $binder Placeholder generator object
+ * @return string
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $parts = [];
+ $parts[] = 'CASE';
+ foreach ($this->_conditions as $k => $part) {
+ $value = $this->_values[$k];
+ $parts[] = 'WHEN ' . $this->_compile($part, $binder) . ' THEN ' . $this->_compile($value, $binder);
+ }
+ if ($this->_elseValue !== null) {
+ $parts[] = 'ELSE';
+ $parts[] = $this->_compile($this->_elseValue, $binder);
+ }
+ $parts[] = 'END';
+
+ return implode(' ', $parts);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ foreach (['_conditions', '_values'] as $part) {
+ foreach ($this->{$part} as $c) {
+ if ($c instanceof ExpressionInterface) {
+ $callback($c);
+ $c->traverse($callback);
+ }
+ }
+ }
+ if ($this->_elseValue instanceof ExpressionInterface) {
+ $callback($this->_elseValue);
+ $this->_elseValue->traverse($callback);
+ }
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/CommonTableExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/CommonTableExpression.php
new file mode 100644
index 000000000..d2a38cadc
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/CommonTableExpression.php
@@ -0,0 +1,239 @@
+name = new IdentifierExpression($name);
+ if ($query) {
+ $this->query($query);
+ }
+ }
+
+ /**
+ * Sets the name of this CTE.
+ *
+ * This is the named you used to reference the expression
+ * in select, insert, etc queries.
+ *
+ * @param string $name The CTE name.
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = new IdentifierExpression($name);
+
+ return $this;
+ }
+
+ /**
+ * Sets the query for this CTE.
+ *
+ * @param \Closure|\Cake\Database\ExpressionInterface $query CTE query
+ * @return $this
+ */
+ public function query($query)
+ {
+ if ($query instanceof Closure) {
+ $query = $query();
+ if (!($query instanceof ExpressionInterface)) {
+ throw new RuntimeException(
+ 'You must return an `ExpressionInterface` from a Closure passed to `query()`.'
+ );
+ }
+ }
+ $this->query = $query;
+
+ return $this;
+ }
+
+ /**
+ * Adds one or more fields (arguments) to the CTE.
+ *
+ * @param string|string[]|\Cake\Database\Expression\IdentifierExpression|\Cake\Database\Expression\IdentifierExpression[] $fields Field names
+ * @return $this
+ */
+ public function field($fields)
+ {
+ $fields = (array)$fields;
+ foreach ($fields as &$field) {
+ if (!($field instanceof IdentifierExpression)) {
+ $field = new IdentifierExpression($field);
+ }
+ }
+ $this->fields = array_merge($this->fields, $fields);
+
+ return $this;
+ }
+
+ /**
+ * Sets this CTE as materialized.
+ *
+ * @return $this
+ */
+ public function materialized()
+ {
+ $this->materialized = 'MATERIALIZED';
+
+ return $this;
+ }
+
+ /**
+ * Sets this CTE as not materialized.
+ *
+ * @return $this
+ */
+ public function notMaterialized()
+ {
+ $this->materialized = 'NOT MATERIALIZED';
+
+ return $this;
+ }
+
+ /**
+ * Gets whether this CTE is recursive.
+ *
+ * @return bool
+ */
+ public function isRecursive(): bool
+ {
+ return $this->recursive;
+ }
+
+ /**
+ * Sets this CTE as recursive.
+ *
+ * @return $this
+ */
+ public function recursive()
+ {
+ $this->recursive = true;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $fields = '';
+ if ($this->fields) {
+ $expressions = array_map(function (IdentifierExpression $e) use ($binder) {
+ return $e->sql($binder);
+ }, $this->fields);
+ $fields = sprintf('(%s)', implode(', ', $expressions));
+ }
+
+ $suffix = $this->materialized ? $this->materialized . ' ' : '';
+
+ return sprintf(
+ '%s%s AS %s(%s)',
+ $this->name->sql($binder),
+ $fields,
+ $suffix,
+ $this->query ? $this->query->sql($binder) : ''
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ $callback($this->name);
+ foreach ($this->fields as $field) {
+ $callback($field);
+ $field->traverse($callback);
+ }
+
+ if ($this->query) {
+ $callback($this->query);
+ $this->query->traverse($callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Clones the inner expression objects.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ $this->name = clone $this->name;
+ if ($this->query) {
+ $this->query = clone $this->query;
+ }
+
+ foreach ($this->fields as $key => $field) {
+ $this->fields[$key] = clone $field;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/Comparison.php b/app/vendor/cakephp/cakephp/src/Database/Expression/Comparison.php
new file mode 100644
index 000000000..11cdb984c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/Comparison.php
@@ -0,0 +1,6 @@
+_type = $type;
+ $this->setField($field);
+ $this->setValue($value);
+ $this->_operator = $operator;
+ }
+
+ /**
+ * Sets the value
+ *
+ * @param mixed $value The value to compare
+ * @return void
+ */
+ public function setValue($value): void
+ {
+ $value = $this->_castToExpression($value, $this->_type);
+
+ $isMultiple = $this->_type && strpos($this->_type, '[]') !== false;
+ if ($isMultiple) {
+ [$value, $this->_valueExpressions] = $this->_collectExpressions($value);
+ }
+
+ $this->_isMultiple = $isMultiple;
+ $this->_value = $value;
+ }
+
+ /**
+ * Returns the value used for comparison
+ *
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->_value;
+ }
+
+ /**
+ * Sets the operator to use for the comparison
+ *
+ * @param string $operator The operator to be used for the comparison.
+ * @return void
+ */
+ public function setOperator(string $operator): void
+ {
+ $this->_operator = $operator;
+ }
+
+ /**
+ * Returns the operator used for comparison
+ *
+ * @return string
+ */
+ public function getOperator(): string
+ {
+ return $this->_operator;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ /** @var string|\Cake\Database\ExpressionInterface $field */
+ $field = $this->_field;
+
+ if ($field instanceof ExpressionInterface) {
+ $field = $field->sql($binder);
+ }
+
+ if ($this->_value instanceof ExpressionInterface) {
+ $template = '%s %s (%s)';
+ $value = $this->_value->sql($binder);
+ } else {
+ [$template, $value] = $this->_stringExpression($binder);
+ }
+
+ return sprintf($template, $field, $this->_operator, $value);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ if ($this->_field instanceof ExpressionInterface) {
+ $callback($this->_field);
+ $this->_field->traverse($callback);
+ }
+
+ if ($this->_value instanceof ExpressionInterface) {
+ $callback($this->_value);
+ $this->_value->traverse($callback);
+ }
+
+ foreach ($this->_valueExpressions as $v) {
+ $callback($v);
+ $v->traverse($callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create a deep clone.
+ *
+ * Clones the field and value if they are expression objects.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ foreach (['_value', '_field'] as $prop) {
+ if ($this->{$prop} instanceof ExpressionInterface) {
+ $this->{$prop} = clone $this->{$prop};
+ }
+ }
+ }
+
+ /**
+ * Returns a template and a placeholder for the value after registering it
+ * with the placeholder $binder
+ *
+ * @param \Cake\Database\ValueBinder $binder The value binder to use.
+ * @return array First position containing the template and the second a placeholder
+ */
+ protected function _stringExpression(ValueBinder $binder): array
+ {
+ $template = '%s ';
+
+ if ($this->_field instanceof ExpressionInterface) {
+ $template = '(%s) ';
+ }
+
+ if ($this->_isMultiple) {
+ $template .= '%s (%s)';
+ $type = $this->_type;
+ if ($type !== null) {
+ $type = str_replace('[]', '', $type);
+ }
+ $value = $this->_flattenValue($this->_value, $binder, $type);
+
+ // To avoid SQL errors when comparing a field to a list of empty values,
+ // better just throw an exception here
+ if ($value === '') {
+ $field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field;
+ /** @psalm-suppress PossiblyInvalidCast */
+ throw new DatabaseException(
+ "Impossible to generate condition with empty list of values for field ($field)"
+ );
+ }
+ } else {
+ $template .= '%s %s';
+ $value = $this->_bindValue($this->_value, $binder, $this->_type);
+ }
+
+ return [$template, $value];
+ }
+
+ /**
+ * Registers a value in the placeholder generator and returns the generated placeholder
+ *
+ * @param mixed $value The value to bind
+ * @param \Cake\Database\ValueBinder $binder The value binder to use
+ * @param string|null $type The type of $value
+ * @return string generated placeholder
+ */
+ protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
+ {
+ $placeholder = $binder->placeholder('c');
+ $binder->bind($placeholder, $value, $type);
+
+ return $placeholder;
+ }
+
+ /**
+ * Converts a traversable value into a set of placeholders generated by
+ * $binder and separated by `,`
+ *
+ * @param iterable $value the value to flatten
+ * @param \Cake\Database\ValueBinder $binder The value binder to use
+ * @param string|null $type the type to cast values to
+ * @return string
+ */
+ protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string
+ {
+ $parts = [];
+ if (is_array($value)) {
+ foreach ($this->_valueExpressions as $k => $v) {
+ $parts[$k] = $v->sql($binder);
+ unset($value[$k]);
+ }
+ }
+
+ if (!empty($value)) {
+ $parts += $binder->generateManyNamed($value, $type);
+ }
+
+ return implode(',', $parts);
+ }
+
+ /**
+ * Returns an array with the original $values in the first position
+ * and all ExpressionInterface objects that could be found in the second
+ * position.
+ *
+ * @param iterable|\Cake\Database\ExpressionInterface $values The rows to insert
+ * @return array
+ */
+ protected function _collectExpressions($values): array
+ {
+ if ($values instanceof ExpressionInterface) {
+ return [$values, []];
+ }
+
+ $expressions = $result = [];
+ $isArray = is_array($values);
+
+ if ($isArray) {
+ /** @var array $result */
+ $result = $values;
+ }
+
+ foreach ($values as $k => $v) {
+ if ($v instanceof ExpressionInterface) {
+ $expressions[$k] = $v;
+ }
+
+ if ($isArray) {
+ $result[$k] = $v;
+ }
+ }
+
+ return [$result, $expressions];
+ }
+}
+
+// phpcs:disable
+// Comparison will not load during instanceof checks so ensure it's loaded here
+// @deprecated 4.1.0 Add backwards compatible alias.
+class_alias('Cake\Database\Expression\ComparisonExpression', 'Cake\Database\Expression\Comparison');
+// phpcs:enable
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/FieldInterface.php b/app/vendor/cakephp/cakephp/src/Database/Expression/FieldInterface.php
new file mode 100644
index 000000000..24b7204f1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/FieldInterface.php
@@ -0,0 +1,39 @@
+_field = $field;
+ }
+
+ /**
+ * Returns the field name
+ *
+ * @return string|array|\Cake\Database\ExpressionInterface
+ */
+ public function getField()
+ {
+ return $this->_field;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/FunctionExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/FunctionExpression.php
new file mode 100644
index 000000000..217e1ebd0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/FunctionExpression.php
@@ -0,0 +1,178 @@
+ 'literal', ' rules']);`
+ *
+ * Will produce `CONCAT(name, ' rules')`
+ *
+ * @param string $name the name of the function to be constructed
+ * @param array $params list of arguments to be passed to the function
+ * If associative the key would be used as argument when value is 'literal'
+ * @param array $types associative array of types to be associated with the
+ * passed arguments
+ * @param string $returnType The return type of this expression
+ */
+ public function __construct(string $name, array $params = [], array $types = [], string $returnType = 'string')
+ {
+ $this->_name = $name;
+ $this->_returnType = $returnType;
+ parent::__construct($params, $types, ',');
+ }
+
+ /**
+ * Sets the name of the SQL function to be invoke in this expression.
+ *
+ * @param string $name The name of the function
+ * @return $this
+ */
+ public function setName(string $name)
+ {
+ $this->_name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Gets the name of the SQL function to be invoke in this expression.
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->_name;
+ }
+
+ /**
+ * Adds one or more arguments for the function call.
+ *
+ * @param array $conditions list of arguments to be passed to the function
+ * If associative the key would be used as argument when value is 'literal'
+ * @param array $types associative array of types to be associated with the
+ * passed arguments
+ * @param bool $prepend Whether to prepend or append to the list of arguments
+ * @see \Cake\Database\Expression\FunctionExpression::__construct() for more details.
+ * @return $this
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ public function add($conditions, array $types = [], bool $prepend = false)
+ {
+ $put = $prepend ? 'array_unshift' : 'array_push';
+ $typeMap = $this->getTypeMap()->setTypes($types);
+ foreach ($conditions as $k => $p) {
+ if ($p === 'literal') {
+ $put($this->_conditions, $k);
+ continue;
+ }
+
+ if ($p === 'identifier') {
+ $put($this->_conditions, new IdentifierExpression($k));
+ continue;
+ }
+
+ $type = $typeMap->type($k);
+
+ if ($type !== null && !$p instanceof ExpressionInterface) {
+ $p = $this->_castToExpression($p, $type);
+ }
+
+ if ($p instanceof ExpressionInterface) {
+ $put($this->_conditions, $p);
+ continue;
+ }
+
+ $put($this->_conditions, ['value' => $p, 'type' => $type]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $parts = [];
+ foreach ($this->_conditions as $condition) {
+ if ($condition instanceof Query) {
+ $condition = sprintf('(%s)', $condition->sql($binder));
+ } elseif ($condition instanceof ExpressionInterface) {
+ $condition = $condition->sql($binder);
+ } elseif (is_array($condition)) {
+ $p = $binder->placeholder('param');
+ $binder->bind($p, $condition['value'], $condition['type']);
+ $condition = $p;
+ }
+ $parts[] = $condition;
+ }
+
+ return $this->_name . sprintf('(%s)', implode(
+ $this->_conjunction . ' ',
+ $parts
+ ));
+ }
+
+ /**
+ * The name of the function is in itself an expression to generate, thus
+ * always adding 1 to the amount of expressions stored in this object.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return 1 + count($this->_conditions);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/IdentifierExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/IdentifierExpression.php
new file mode 100644
index 000000000..69486a41f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/IdentifierExpression.php
@@ -0,0 +1,119 @@
+_identifier = $identifier;
+ $this->collation = $collation;
+ }
+
+ /**
+ * Sets the identifier this expression represents
+ *
+ * @param string $identifier The identifier
+ * @return void
+ */
+ public function setIdentifier(string $identifier): void
+ {
+ $this->_identifier = $identifier;
+ }
+
+ /**
+ * Returns the identifier this expression represents
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return $this->_identifier;
+ }
+
+ /**
+ * Sets the collation.
+ *
+ * @param string $collation Identifier collation
+ * @return void
+ */
+ public function setCollation(string $collation): void
+ {
+ $this->collation = $collation;
+ }
+
+ /**
+ * Returns the collation.
+ *
+ * @return string|null
+ */
+ public function getCollation(): ?string
+ {
+ return $this->collation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $sql = $this->_identifier;
+ if ($this->collation) {
+ $sql .= ' COLLATE ' . $this->collation;
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/OrderByExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderByExpression.php
new file mode 100644
index 000000000..dd7381144
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderByExpression.php
@@ -0,0 +1,88 @@
+_conditions as $k => $direction) {
+ if ($direction instanceof ExpressionInterface) {
+ $direction = $direction->sql($binder);
+ }
+ $order[] = is_numeric($k) ? $direction : sprintf('%s %s', $k, $direction);
+ }
+
+ return sprintf('ORDER BY %s', implode(', ', $order));
+ }
+
+ /**
+ * Auxiliary function used for decomposing a nested array of conditions and
+ * building a tree structure inside this object to represent the full SQL expression.
+ *
+ * New order by expressions are merged to existing ones
+ *
+ * @param array $conditions list of order by expressions
+ * @param array $types list of types associated on fields referenced in $conditions
+ * @return void
+ */
+ protected function _addConditions(array $conditions, array $types): void
+ {
+ foreach ($conditions as $key => $val) {
+ if (
+ is_string($key) &&
+ is_string($val) &&
+ !in_array(strtoupper($val), ['ASC', 'DESC'], true)
+ ) {
+ throw new RuntimeException(
+ sprintf(
+ 'Passing extra expressions by associative array (`\'%s\' => \'%s\'`) ' .
+ 'is not allowed to avoid potential SQL injection. ' .
+ 'Use QueryExpression or numeric array instead.',
+ $key,
+ $val
+ )
+ );
+ }
+ }
+
+ $this->_conditions = array_merge($this->_conditions, $conditions);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/OrderClauseExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderClauseExpression.php
new file mode 100644
index 000000000..5e7b84058
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/OrderClauseExpression.php
@@ -0,0 +1,90 @@
+_field = $field;
+ $this->_direction = strtolower($direction) === 'asc' ? 'ASC' : 'DESC';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ /** @var string|\Cake\Database\ExpressionInterface $field */
+ $field = $this->_field;
+ if ($field instanceof Query) {
+ $field = sprintf('(%s)', $field->sql($binder));
+ } elseif ($field instanceof ExpressionInterface) {
+ $field = $field->sql($binder);
+ }
+
+ return sprintf('%s %s', $field, $this->_direction);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ if ($this->_field instanceof ExpressionInterface) {
+ $callback($this->_field);
+ $this->_field->traverse($callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create a deep clone of the order clause.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ if ($this->_field instanceof ExpressionInterface) {
+ $this->_field = clone $this->_field;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/QueryExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/QueryExpression.php
new file mode 100644
index 000000000..3f3ec4ff8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/QueryExpression.php
@@ -0,0 +1,824 @@
+ :value"
+ *
+ * @var array
+ */
+ protected $_conditions = [];
+
+ /**
+ * Constructor. A new expression object can be created without any params and
+ * be built dynamically. Otherwise it is possible to pass an array of conditions
+ * containing either a tree-like array structure to be parsed and/or other
+ * expression objects. Optionally, you can set the conjunction keyword to be used
+ * for joining each part of this level of the expression tree.
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions Tree like array structure
+ * containing all the conditions to be added or nested inside this expression object.
+ * @param array|\Cake\Database\TypeMap $types associative array of types to be associated with the values
+ * passed in $conditions.
+ * @param string $conjunction the glue that will join all the string conditions at this
+ * level of the expression tree. For example "AND", "OR", "XOR"...
+ * @see \Cake\Database\Expression\QueryExpression::add() for more details on $conditions and $types
+ */
+ public function __construct($conditions = [], $types = [], $conjunction = 'AND')
+ {
+ $this->setTypeMap($types);
+ $this->setConjunction(strtoupper($conjunction));
+ if (!empty($conditions)) {
+ $this->add($conditions, $this->getTypeMap()->getTypes());
+ }
+ }
+
+ /**
+ * Changes the conjunction for the conditions at this level of the expression tree.
+ *
+ * @param string $conjunction Value to be used for joining conditions
+ * @return $this
+ */
+ public function setConjunction(string $conjunction)
+ {
+ $this->_conjunction = strtoupper($conjunction);
+
+ return $this;
+ }
+
+ /**
+ * Gets the currently configured conjunction for the conditions at this level of the expression tree.
+ *
+ * @return string
+ */
+ public function getConjunction(): string
+ {
+ return $this->_conjunction;
+ }
+
+ /**
+ * Adds one or more conditions to this expression object. Conditions can be
+ * expressed in a one dimensional array, that will cause all conditions to
+ * be added directly at this level of the tree or they can be nested arbitrarily
+ * making it create more expression objects that will be nested inside and
+ * configured to use the specified conjunction.
+ *
+ * If the type passed for any of the fields is expressed "type[]" (note braces)
+ * then it will cause the placeholder to be re-written dynamically so if the
+ * value is an array, it will create as many placeholders as values are in it.
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions single or multiple conditions to
+ * be added. When using an array and the key is 'OR' or 'AND' a new expression
+ * object will be created with that conjunction and internal array value passed
+ * as conditions.
+ * @param array $types associative array of fields pointing to the type of the
+ * values that are being passed. Used for correctly binding values to statements.
+ * @see \Cake\Database\Query::where() for examples on conditions
+ * @return $this
+ */
+ public function add($conditions, array $types = [])
+ {
+ if (is_string($conditions)) {
+ $this->_conditions[] = $conditions;
+
+ return $this;
+ }
+
+ if ($conditions instanceof ExpressionInterface) {
+ $this->_conditions[] = $conditions;
+
+ return $this;
+ }
+
+ $this->_addConditions($conditions, $types);
+
+ return $this;
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field = value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * If it is suffixed with "[]" and the value is an array then multiple placeholders
+ * will be created, one per each value in the array.
+ * @return $this
+ */
+ public function eq($field, $value, ?string $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, '='));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field != value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * If it is suffixed with "[]" and the value is an array then multiple placeholders
+ * will be created, one per each value in the array.
+ * @return $this
+ */
+ public function notEq($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, '!='));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field > value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function gt($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, '>'));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field < value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function lt($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, '<'));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field >= value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function gte($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, '>='));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field <= value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function lte($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, '<='));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field IS NULL".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field database field to be
+ * tested for null
+ * @return $this
+ */
+ public function isNull($field)
+ {
+ if (!($field instanceof ExpressionInterface)) {
+ $field = new IdentifierExpression($field);
+ }
+
+ return $this->add(new UnaryExpression('IS NULL', $field, UnaryExpression::POSTFIX));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field IS NOT NULL".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field database field to be
+ * tested for not null
+ * @return $this
+ */
+ public function isNotNull($field)
+ {
+ if (!($field instanceof ExpressionInterface)) {
+ $field = new IdentifierExpression($field);
+ }
+
+ return $this->add(new UnaryExpression('IS NOT NULL', $field, UnaryExpression::POSTFIX));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field LIKE value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function like($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, 'LIKE'));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "field NOT LIKE value".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param mixed $value The value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function notLike($field, $value, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new ComparisonExpression($field, $value, $type, 'NOT LIKE'));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form
+ * "field IN (value1, value2)".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param string|array|\Cake\Database\ExpressionInterface $values the value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function in($field, $values, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+ $type = $type ?: 'string';
+ $type .= '[]';
+ $values = $values instanceof ExpressionInterface ? $values : (array)$values;
+
+ return $this->add(new ComparisonExpression($field, $values, $type, 'IN'));
+ }
+
+ /**
+ * Adds a new case expression to the expression object
+ *
+ * @param array|\Cake\Database\ExpressionInterface $conditions The conditions to test. Must be a ExpressionInterface
+ * instance, or an array of ExpressionInterface instances.
+ * @param array|\Cake\Database\ExpressionInterface $values associative array of values to be associated with the
+ * conditions passed in $conditions. If there are more $values than $conditions,
+ * the last $value is used as the `ELSE` value.
+ * @param array $types associative array of types to be associated with the values
+ * passed in $values
+ * @return $this
+ */
+ public function addCase($conditions, $values = [], $types = [])
+ {
+ return $this->add(new CaseExpression($conditions, $values, $types));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form
+ * "field NOT IN (value1, value2)".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Database field to be compared against value
+ * @param string|array|\Cake\Database\ExpressionInterface $values the value to be bound to $field for comparison
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function notIn($field, $values, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+ $type = $type ?: 'string';
+ $type .= '[]';
+ $values = $values instanceof ExpressionInterface ? $values : (array)$values;
+
+ return $this->add(new ComparisonExpression($field, $values, $type, 'NOT IN'));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "EXISTS (...)".
+ *
+ * @param \Cake\Database\ExpressionInterface $expression the inner query
+ * @return $this
+ */
+ public function exists(ExpressionInterface $expression)
+ {
+ return $this->add(new UnaryExpression('EXISTS', $expression, UnaryExpression::PREFIX));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form "NOT EXISTS (...)".
+ *
+ * @param \Cake\Database\ExpressionInterface $expression the inner query
+ * @return $this
+ */
+ public function notExists(ExpressionInterface $expression)
+ {
+ return $this->add(new UnaryExpression('NOT EXISTS', $expression, UnaryExpression::PREFIX));
+ }
+
+ /**
+ * Adds a new condition to the expression object in the form
+ * "field BETWEEN from AND to".
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field The field name to compare for values inbetween the range.
+ * @param mixed $from The initial value of the range.
+ * @param mixed $to The ending value in the comparison range.
+ * @param string|null $type the type name for $value as configured using the Type map.
+ * @return $this
+ */
+ public function between($field, $from, $to, $type = null)
+ {
+ if ($type === null) {
+ $type = $this->_calculateType($field);
+ }
+
+ return $this->add(new BetweenExpression($field, $from, $to, $type));
+ }
+
+ /**
+ * Returns a new QueryExpression object containing all the conditions passed
+ * and set up the conjunction to be "AND"
+ *
+ * @param \Closure|string|array|\Cake\Database\ExpressionInterface $conditions to be joined with AND
+ * @param array $types associative array of fields pointing to the type of the
+ * values that are being passed. Used for correctly binding values to statements.
+ * @return \Cake\Database\Expression\QueryExpression
+ */
+ public function and($conditions, $types = [])
+ {
+ if ($conditions instanceof Closure) {
+ return $conditions(new static([], $this->getTypeMap()->setTypes($types)));
+ }
+
+ return new static($conditions, $this->getTypeMap()->setTypes($types));
+ }
+
+ /**
+ * Returns a new QueryExpression object containing all the conditions passed
+ * and set up the conjunction to be "OR"
+ *
+ * @param \Closure|string|array|\Cake\Database\ExpressionInterface $conditions to be joined with OR
+ * @param array $types associative array of fields pointing to the type of the
+ * values that are being passed. Used for correctly binding values to statements.
+ * @return \Cake\Database\Expression\QueryExpression
+ */
+ public function or($conditions, $types = [])
+ {
+ if ($conditions instanceof Closure) {
+ return $conditions(new static([], $this->getTypeMap()->setTypes($types), 'OR'));
+ }
+
+ return new static($conditions, $this->getTypeMap()->setTypes($types), 'OR');
+ }
+
+ // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
+
+ /**
+ * Returns a new QueryExpression object containing all the conditions passed
+ * and set up the conjunction to be "AND"
+ *
+ * @param \Closure|string|array|\Cake\Database\ExpressionInterface $conditions to be joined with AND
+ * @param array $types associative array of fields pointing to the type of the
+ * values that are being passed. Used for correctly binding values to statements.
+ * @return \Cake\Database\Expression\QueryExpression
+ * @deprecated 4.0.0 Use {@link and()} instead.
+ */
+ public function and_($conditions, $types = [])
+ {
+ deprecationWarning('QueryExpression::and_() is deprecated use and() instead.');
+
+ return $this->and($conditions, $types);
+ }
+
+ /**
+ * Returns a new QueryExpression object containing all the conditions passed
+ * and set up the conjunction to be "OR"
+ *
+ * @param \Closure|string|array|\Cake\Database\ExpressionInterface $conditions to be joined with OR
+ * @param array $types associative array of fields pointing to the type of the
+ * values that are being passed. Used for correctly binding values to statements.
+ * @return \Cake\Database\Expression\QueryExpression
+ * @deprecated 4.0.0 Use {@link or()} instead.
+ */
+ public function or_($conditions, $types = [])
+ {
+ deprecationWarning('QueryExpression::or_() is deprecated use or() instead.');
+
+ return $this->or($conditions, $types);
+ }
+
+ // phpcs:enable
+
+ /**
+ * Adds a new set of conditions to this level of the tree and negates
+ * the final result by prepending a NOT, it will look like
+ * "NOT ( (condition1) AND (conditions2) )" conjunction depends on the one
+ * currently configured for this object.
+ *
+ * @param string|array|\Closure|\Cake\Database\ExpressionInterface $conditions to be added and negated
+ * @param array $types associative array of fields pointing to the type of the
+ * values that are being passed. Used for correctly binding values to statements.
+ * @return $this
+ */
+ public function not($conditions, $types = [])
+ {
+ return $this->add(['NOT' => $conditions], $types);
+ }
+
+ /**
+ * Returns the number of internal conditions that are stored in this expression.
+ * Useful to determine if this expression object is void or it will generate
+ * a non-empty string when compiled
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->_conditions);
+ }
+
+ /**
+ * Builds equal condition or assignment with identifier wrapping.
+ *
+ * @param string $leftField Left join condition field name.
+ * @param string $rightField Right join condition field name.
+ * @return $this
+ */
+ public function equalFields(string $leftField, string $rightField)
+ {
+ $wrapIdentifier = function ($field) {
+ if ($field instanceof ExpressionInterface) {
+ return $field;
+ }
+
+ return new IdentifierExpression($field);
+ };
+
+ return $this->eq($wrapIdentifier($leftField), $wrapIdentifier($rightField));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $len = $this->count();
+ if ($len === 0) {
+ return '';
+ }
+ $conjunction = $this->_conjunction;
+ $template = $len === 1 ? '%s' : '(%s)';
+ $parts = [];
+ foreach ($this->_conditions as $part) {
+ if ($part instanceof Query) {
+ $part = '(' . $part->sql($binder) . ')';
+ } elseif ($part instanceof ExpressionInterface) {
+ $part = $part->sql($binder);
+ }
+ if (strlen($part)) {
+ $parts[] = $part;
+ }
+ }
+
+ return sprintf($template, implode(" $conjunction ", $parts));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ foreach ($this->_conditions as $c) {
+ if ($c instanceof ExpressionInterface) {
+ $callback($c);
+ $c->traverse($callback);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Executes a callable function for each of the parts that form this expression.
+ *
+ * The callable function is required to return a value with which the currently
+ * visited part will be replaced. If the callable function returns null then
+ * the part will be discarded completely from this expression.
+ *
+ * The callback function will receive each of the conditions as first param and
+ * the key as second param. It is possible to declare the second parameter as
+ * passed by reference, this will enable you to change the key under which the
+ * modified part is stored.
+ *
+ * @param callable $callback The callable to apply to each part.
+ * @return $this
+ */
+ public function iterateParts(callable $callback)
+ {
+ $parts = [];
+ foreach ($this->_conditions as $k => $c) {
+ $key = &$k;
+ $part = $callback($c, $key);
+ if ($part !== null) {
+ $parts[$key] = $part;
+ }
+ }
+ $this->_conditions = $parts;
+
+ return $this;
+ }
+
+ /**
+ * Check whether or not a callable is acceptable.
+ *
+ * We don't accept ['class', 'method'] style callbacks,
+ * as they often contain user input and arrays of strings
+ * are easy to sneak in.
+ *
+ * @param callable|string|array|\Cake\Database\ExpressionInterface $callable The callable to check.
+ * @return bool Valid callable.
+ * @deprecated 4.2.0 This method is unused.
+ * @codeCoverageIgnore
+ */
+ public function isCallable($callable): bool
+ {
+ if (is_string($callable)) {
+ return false;
+ }
+ if (is_object($callable) && is_callable($callable)) {
+ return true;
+ }
+
+ return is_array($callable) && isset($callable[0]) && is_object($callable[0]) && is_callable($callable);
+ }
+
+ /**
+ * Returns true if this expression contains any other nested
+ * ExpressionInterface objects
+ *
+ * @return bool
+ */
+ public function hasNestedExpression(): bool
+ {
+ foreach ($this->_conditions as $c) {
+ if ($c instanceof ExpressionInterface) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Auxiliary function used for decomposing a nested array of conditions and build
+ * a tree structure inside this object to represent the full SQL expression.
+ * String conditions are stored directly in the conditions, while any other
+ * representation is wrapped around an adequate instance or of this class.
+ *
+ * @param array $conditions list of conditions to be stored in this object
+ * @param array $types list of types associated on fields referenced in $conditions
+ * @return void
+ */
+ protected function _addConditions(array $conditions, array $types): void
+ {
+ $operators = ['and', 'or', 'xor'];
+
+ $typeMap = $this->getTypeMap()->setTypes($types);
+
+ foreach ($conditions as $k => $c) {
+ $numericKey = is_numeric($k);
+
+ if ($c instanceof Closure) {
+ $expr = new static([], $typeMap);
+ $c = $c($expr, $this);
+ }
+
+ if ($numericKey && empty($c)) {
+ continue;
+ }
+
+ $isArray = is_array($c);
+ $isOperator = $isNot = false;
+ if (!$numericKey) {
+ $normalizedKey = strtolower($k);
+ $isOperator = in_array($normalizedKey, $operators);
+ $isNot = $normalizedKey === 'not';
+ }
+
+ if (($isOperator || $isNot) && ($isArray || $c instanceof Countable) && count($c) === 0) {
+ continue;
+ }
+
+ if ($numericKey && $c instanceof ExpressionInterface) {
+ $this->_conditions[] = $c;
+ continue;
+ }
+
+ if ($numericKey && is_string($c)) {
+ $this->_conditions[] = $c;
+ continue;
+ }
+
+ if ($numericKey && $isArray || $isOperator) {
+ $this->_conditions[] = new static($c, $typeMap, $numericKey ? 'AND' : $k);
+ continue;
+ }
+
+ if ($isNot) {
+ $this->_conditions[] = new UnaryExpression('NOT', new static($c, $typeMap));
+ continue;
+ }
+
+ if (!$numericKey) {
+ $this->_conditions[] = $this->_parseCondition($k, $c);
+ }
+ }
+ }
+
+ /**
+ * Parses a string conditions by trying to extract the operator inside it if any
+ * and finally returning either an adequate QueryExpression object or a plain
+ * string representation of the condition. This function is responsible for
+ * generating the placeholders and replacing the values by them, while storing
+ * the value elsewhere for future binding.
+ *
+ * @param string $field The value from which the actual field and operator will
+ * be extracted.
+ * @param mixed $value The value to be bound to a placeholder for the field
+ * @return string|\Cake\Database\ExpressionInterface
+ * @throws \InvalidArgumentException If operator is invalid or missing on NULL usage.
+ */
+ protected function _parseCondition(string $field, $value)
+ {
+ $field = trim($field);
+ $operator = '=';
+ $expression = $field;
+
+ $spaces = substr_count($field, ' ');
+ // Handle operators with a space in them like `is not` and `not like`
+ if ($spaces > 1) {
+ $parts = explode(' ', $field);
+ if (preg_match('/(is not|not \w+)$/i', $field)) {
+ $last = array_pop($parts);
+ $second = array_pop($parts);
+ array_push($parts, strtolower("{$second} {$last}"));
+ }
+ $operator = array_pop($parts);
+ $expression = implode(' ', $parts);
+ } elseif ($spaces == 1) {
+ $parts = explode(' ', $field, 2);
+ [$expression, $operator] = $parts;
+ $operator = strtolower(trim($operator));
+ }
+ $type = $this->getTypeMap()->type($expression);
+
+ $typeMultiple = (is_string($type) && strpos($type, '[]') !== false);
+ if (in_array($operator, ['in', 'not in']) || $typeMultiple) {
+ $type = $type ?: 'string';
+ if (!$typeMultiple) {
+ $type .= '[]';
+ }
+ $operator = $operator === '=' ? 'IN' : $operator;
+ $operator = $operator === '!=' ? 'NOT IN' : $operator;
+ $typeMultiple = true;
+ }
+
+ if ($typeMultiple) {
+ $value = $value instanceof ExpressionInterface ? $value : (array)$value;
+ }
+
+ if ($operator === 'is' && $value === null) {
+ return new UnaryExpression(
+ 'IS NULL',
+ new IdentifierExpression($expression),
+ UnaryExpression::POSTFIX
+ );
+ }
+
+ if ($operator === 'is not' && $value === null) {
+ return new UnaryExpression(
+ 'IS NOT NULL',
+ new IdentifierExpression($expression),
+ UnaryExpression::POSTFIX
+ );
+ }
+
+ if ($operator === 'is' && $value !== null) {
+ $operator = '=';
+ }
+
+ if ($operator === 'is not' && $value !== null) {
+ $operator = '!=';
+ }
+
+ if ($value === null && $this->_conjunction !== ',') {
+ throw new InvalidArgumentException(
+ sprintf('Expression `%s` is missing operator (IS, IS NOT) with `null` value.', $expression)
+ );
+ }
+
+ return new ComparisonExpression($expression, $value, $type, $operator);
+ }
+
+ /**
+ * Returns the type name for the passed field if it was stored in the typeMap
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field The field name to get a type for.
+ * @return string|null The computed type or null, if the type is unknown.
+ */
+ protected function _calculateType($field): ?string
+ {
+ $field = $field instanceof IdentifierExpression ? $field->getIdentifier() : $field;
+ if (is_string($field)) {
+ return $this->getTypeMap()->type($field);
+ }
+
+ return null;
+ }
+
+ /**
+ * Clone this object and its subtree of expressions.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ foreach ($this->_conditions as $i => $condition) {
+ if ($condition instanceof ExpressionInterface) {
+ $this->_conditions[$i] = clone $condition;
+ }
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/StringExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/StringExpression.php
new file mode 100644
index 000000000..e4380744b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/StringExpression.php
@@ -0,0 +1,87 @@
+string = $string;
+ $this->collation = $collation;
+ }
+
+ /**
+ * Sets the string collation.
+ *
+ * @param string $collation String collation
+ * @return void
+ */
+ public function setCollation(string $collation): void
+ {
+ $this->collation = $collation;
+ }
+
+ /**
+ * Returns the string collation.
+ *
+ * @return string
+ */
+ public function getCollation(): string
+ {
+ return $this->collation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $placeholder = $binder->placeholder('c');
+ $binder->bind($placeholder, $this->string, 'string');
+
+ return $placeholder . ' COLLATE ' . $this->collation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/TupleComparison.php b/app/vendor/cakephp/cakephp/src/Database/Expression/TupleComparison.php
new file mode 100644
index 000000000..8896e337e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/TupleComparison.php
@@ -0,0 +1,216 @@
+
+ * @psalm-suppress NonInvariantDocblockPropertyType
+ */
+ protected $_type;
+
+ /**
+ * Constructor
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface $fields the fields to use to form a tuple
+ * @param array|\Cake\Database\ExpressionInterface $values the values to use to form a tuple
+ * @param array $types the types names to use for casting each of the values, only
+ * one type per position in the value array in needed
+ * @param string $conjunction the operator used for comparing field and value
+ */
+ public function __construct($fields, $values, array $types = [], string $conjunction = '=')
+ {
+ $this->_type = $types;
+ $this->setField($fields);
+ $this->setValue($values);
+ $this->_operator = $conjunction;
+ }
+
+ /**
+ * Returns the type to be used for casting the value to a database representation
+ *
+ * @return array
+ */
+ public function getType(): array
+ {
+ return $this->_type;
+ }
+
+ /**
+ * Sets the value
+ *
+ * @param mixed $value The value to compare
+ * @return void
+ */
+ public function setValue($value): void
+ {
+ $this->_value = $value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $template = '(%s) %s (%s)';
+ $fields = [];
+ $originalFields = $this->getField();
+
+ if (!is_array($originalFields)) {
+ $originalFields = [$originalFields];
+ }
+
+ foreach ($originalFields as $field) {
+ $fields[] = $field instanceof ExpressionInterface ? $field->sql($binder) : $field;
+ }
+
+ $values = $this->_stringifyValues($binder);
+
+ $field = implode(', ', $fields);
+
+ return sprintf($template, $field, $this->_operator, $values);
+ }
+
+ /**
+ * Returns a string with the values as placeholders in a string to be used
+ * for the SQL version of this expression
+ *
+ * @param \Cake\Database\ValueBinder $binder The value binder to convert expressions with.
+ * @return string
+ */
+ protected function _stringifyValues(ValueBinder $binder): string
+ {
+ $values = [];
+ $parts = $this->getValue();
+
+ if ($parts instanceof ExpressionInterface) {
+ return $parts->sql($binder);
+ }
+
+ foreach ($parts as $i => $value) {
+ if ($value instanceof ExpressionInterface) {
+ $values[] = $value->sql($binder);
+ continue;
+ }
+
+ $type = $this->_type;
+ $isMultiOperation = $this->isMulti();
+ if (empty($type)) {
+ $type = null;
+ }
+
+ if ($isMultiOperation) {
+ $bound = [];
+ foreach ($value as $k => $val) {
+ /** @var string $valType */
+ $valType = $type && isset($type[$k]) ? $type[$k] : $type;
+ $bound[] = $this->_bindValue($val, $binder, $valType);
+ }
+
+ $values[] = sprintf('(%s)', implode(',', $bound));
+ continue;
+ }
+
+ /** @var string $valType */
+ $valType = $type && isset($type[$i]) ? $type[$i] : $type;
+ $values[] = $this->_bindValue($value, $binder, $valType);
+ }
+
+ return implode(', ', $values);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
+ {
+ $placeholder = $binder->placeholder('tuple');
+ $binder->bind($placeholder, $value, $type);
+
+ return $placeholder;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ /** @var string[] $fields */
+ $fields = $this->getField();
+ foreach ($fields as $field) {
+ $this->_traverseValue($field, $callback);
+ }
+
+ $value = $this->getValue();
+ if ($value instanceof ExpressionInterface) {
+ $callback($value);
+ $value->traverse($callback);
+
+ return $this;
+ }
+
+ foreach ($value as $val) {
+ if ($this->isMulti()) {
+ foreach ($val as $v) {
+ $this->_traverseValue($v, $callback);
+ }
+ } else {
+ $this->_traverseValue($val, $callback);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Conditionally executes the callback for the passed value if
+ * it is an ExpressionInterface
+ *
+ * @param mixed $value The value to traverse
+ * @param \Closure $callback The callable to use when traversing
+ * @return void
+ */
+ protected function _traverseValue($value, Closure $callback): void
+ {
+ if ($value instanceof ExpressionInterface) {
+ $callback($value);
+ $value->traverse($callback);
+ }
+ }
+
+ /**
+ * Determines if each of the values in this expressions is a tuple in
+ * itself
+ *
+ * @return bool
+ */
+ public function isMulti(): bool
+ {
+ return in_array(strtolower($this->_operator), ['in', 'not in']);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/UnaryExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/UnaryExpression.php
new file mode 100644
index 000000000..1c40f913f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/UnaryExpression.php
@@ -0,0 +1,118 @@
+_operator = $operator;
+ $this->_value = $value;
+ $this->position = $position;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $operand = $this->_value;
+ if ($operand instanceof ExpressionInterface) {
+ $operand = $operand->sql($binder);
+ }
+
+ if ($this->position === self::POSTFIX) {
+ return '(' . $operand . ') ' . $this->_operator;
+ }
+
+ return $this->_operator . ' (' . $operand . ')';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ if ($this->_value instanceof ExpressionInterface) {
+ $callback($this->_value);
+ $this->_value->traverse($callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Perform a deep clone of the inner expression.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ if ($this->_value instanceof ExpressionInterface) {
+ $this->_value = clone $this->_value;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/ValuesExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/ValuesExpression.php
new file mode 100644
index 000000000..8219de365
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/ValuesExpression.php
@@ -0,0 +1,325 @@
+ type names
+ */
+ public function __construct(array $columns, TypeMap $typeMap)
+ {
+ $this->_columns = $columns;
+ $this->setTypeMap($typeMap);
+ }
+
+ /**
+ * Add a row of data to be inserted.
+ *
+ * @param array|\Cake\Database\Query $values Array of data to append into the insert, or
+ * a query for doing INSERT INTO .. SELECT style commands
+ * @return void
+ * @throws \Cake\Database\Exception\DatabaseException When mixing array + Query data types.
+ */
+ public function add($values): void
+ {
+ if (
+ (
+ count($this->_values) &&
+ $values instanceof Query
+ ) ||
+ (
+ $this->_query &&
+ is_array($values)
+ )
+ ) {
+ throw new DatabaseException(
+ 'You cannot mix subqueries and array values in inserts.'
+ );
+ }
+ if ($values instanceof Query) {
+ $this->setQuery($values);
+
+ return;
+ }
+ $this->_values[] = $values;
+ $this->_castedExpressions = false;
+ }
+
+ /**
+ * Sets the columns to be inserted.
+ *
+ * @param array $columns Array with columns to be inserted.
+ * @return $this
+ */
+ public function setColumns(array $columns)
+ {
+ $this->_columns = $columns;
+ $this->_castedExpressions = false;
+
+ return $this;
+ }
+
+ /**
+ * Gets the columns to be inserted.
+ *
+ * @return array
+ */
+ public function getColumns(): array
+ {
+ return $this->_columns;
+ }
+
+ /**
+ * Get the bare column names.
+ *
+ * Because column names could be identifier quoted, we
+ * need to strip the identifiers off of the columns.
+ *
+ * @return array
+ */
+ protected function _columnNames(): array
+ {
+ $columns = [];
+ foreach ($this->_columns as $col) {
+ if (is_string($col)) {
+ $col = trim($col, '`[]"');
+ }
+ $columns[] = $col;
+ }
+
+ return $columns;
+ }
+
+ /**
+ * Sets the values to be inserted.
+ *
+ * @param array $values Array with values to be inserted.
+ * @return $this
+ */
+ public function setValues(array $values)
+ {
+ $this->_values = $values;
+ $this->_castedExpressions = false;
+
+ return $this;
+ }
+
+ /**
+ * Gets the values to be inserted.
+ *
+ * @return array
+ */
+ public function getValues(): array
+ {
+ if (!$this->_castedExpressions) {
+ $this->_processExpressions();
+ }
+
+ return $this->_values;
+ }
+
+ /**
+ * Sets the query object to be used as the values expression to be evaluated
+ * to insert records in the table.
+ *
+ * @param \Cake\Database\Query $query The query to set
+ * @return $this
+ */
+ public function setQuery(Query $query)
+ {
+ $this->_query = $query;
+
+ return $this;
+ }
+
+ /**
+ * Gets the query object to be used as the values expression to be evaluated
+ * to insert records in the table.
+ *
+ * @return \Cake\Database\Query|null
+ */
+ public function getQuery(): ?Query
+ {
+ return $this->_query;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ if (empty($this->_values) && empty($this->_query)) {
+ return '';
+ }
+
+ if (!$this->_castedExpressions) {
+ $this->_processExpressions();
+ }
+
+ $columns = $this->_columnNames();
+ $defaults = array_fill_keys($columns, null);
+ $placeholders = [];
+
+ $types = [];
+ $typeMap = $this->getTypeMap();
+ foreach ($defaults as $col => $v) {
+ $types[$col] = $typeMap->type($col);
+ }
+
+ foreach ($this->_values as $row) {
+ $row += $defaults;
+ $rowPlaceholders = [];
+
+ foreach ($columns as $column) {
+ $value = $row[$column];
+
+ if ($value instanceof ExpressionInterface) {
+ $rowPlaceholders[] = '(' . $value->sql($binder) . ')';
+ continue;
+ }
+
+ $placeholder = $binder->placeholder('c');
+ $rowPlaceholders[] = $placeholder;
+ $binder->bind($placeholder, $value, $types[$column]);
+ }
+
+ $placeholders[] = implode(', ', $rowPlaceholders);
+ }
+
+ $query = $this->getQuery();
+ if ($query) {
+ return ' ' . $query->sql($binder);
+ }
+
+ return sprintf(' VALUES (%s)', implode('), (', $placeholders));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ if ($this->_query) {
+ return $this;
+ }
+
+ if (!$this->_castedExpressions) {
+ $this->_processExpressions();
+ }
+
+ foreach ($this->_values as $v) {
+ if ($v instanceof ExpressionInterface) {
+ $v->traverse($callback);
+ }
+ if (!is_array($v)) {
+ continue;
+ }
+ foreach ($v as $field) {
+ if ($field instanceof ExpressionInterface) {
+ $callback($field);
+ $field->traverse($callback);
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Converts values that need to be casted to expressions
+ *
+ * @return void
+ */
+ protected function _processExpressions(): void
+ {
+ $types = [];
+ $typeMap = $this->getTypeMap();
+
+ $columns = $this->_columnNames();
+ foreach ($columns as $c) {
+ if (!is_string($c) && !is_int($c)) {
+ continue;
+ }
+ $types[$c] = $typeMap->type($c);
+ }
+
+ $types = $this->_requiresToExpressionCasting($types);
+
+ if (empty($types)) {
+ return;
+ }
+
+ foreach ($this->_values as $row => $values) {
+ foreach ($types as $col => $type) {
+ /** @var \Cake\Database\Type\ExpressionTypeInterface $type */
+ $this->_values[$row][$col] = $type->toExpression($values[$col]);
+ }
+ }
+ $this->_castedExpressions = true;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/WindowExpression.php b/app/vendor/cakephp/cakephp/src/Database/Expression/WindowExpression.php
new file mode 100644
index 000000000..cfbf53c14
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/WindowExpression.php
@@ -0,0 +1,337 @@
+name = new IdentifierExpression($name);
+ }
+
+ /**
+ * Return whether is only a named window expression.
+ *
+ * These window expressions only specify a named window and do not
+ * specify their own partitions, frame or order.
+ *
+ * @return bool
+ */
+ public function isNamedOnly(): bool
+ {
+ return $this->name->getIdentifier() && (!$this->partitions && !$this->frame && !$this->order);
+ }
+
+ /**
+ * Sets the window name.
+ *
+ * @param string $name Window name
+ * @return $this
+ */
+ public function name(string $name)
+ {
+ $this->name = new IdentifierExpression($name);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function partition($partitions)
+ {
+ if (!$partitions) {
+ return $this;
+ }
+
+ if ($partitions instanceof Closure) {
+ $partitions = $partitions(new QueryExpression([], [], ''));
+ }
+
+ if (!is_array($partitions)) {
+ $partitions = [$partitions];
+ }
+
+ foreach ($partitions as &$partition) {
+ if (is_string($partition)) {
+ $partition = new IdentifierExpression($partition);
+ }
+ }
+
+ $this->partitions = array_merge($this->partitions, $partitions);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function order($fields)
+ {
+ if (!$fields) {
+ return $this;
+ }
+
+ if ($this->order === null) {
+ $this->order = new OrderByExpression();
+ }
+
+ if ($fields instanceof Closure) {
+ $fields = $fields(new QueryExpression([], [], ''));
+ }
+
+ $this->order->add($fields);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function range($start, $end = 0)
+ {
+ return $this->frame(self::RANGE, $start, self::PRECEDING, $end, self::FOLLOWING);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function rows(?int $start, ?int $end = 0)
+ {
+ return $this->frame(self::ROWS, $start, self::PRECEDING, $end, self::FOLLOWING);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function groups(?int $start, ?int $end = 0)
+ {
+ return $this->frame(self::GROUPS, $start, self::PRECEDING, $end, self::FOLLOWING);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function frame(
+ string $type,
+ $startOffset,
+ string $startDirection,
+ $endOffset,
+ string $endDirection
+ ) {
+ $this->frame = [
+ 'type' => $type,
+ 'start' => [
+ 'offset' => $startOffset,
+ 'direction' => $startDirection,
+ ],
+ 'end' => [
+ 'offset' => $endOffset,
+ 'direction' => $endDirection,
+ ],
+ ];
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function excludeCurrent()
+ {
+ $this->exclusion = 'CURRENT ROW';
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function excludeGroup()
+ {
+ $this->exclusion = 'GROUP';
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function excludeTies()
+ {
+ $this->exclusion = 'TIES';
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(ValueBinder $binder): string
+ {
+ $clauses = [];
+ if ($this->name->getIdentifier()) {
+ $clauses[] = $this->name->sql($binder);
+ }
+
+ if ($this->partitions) {
+ $expressions = [];
+ foreach ($this->partitions as $partition) {
+ $expressions[] = $partition->sql($binder);
+ }
+
+ $clauses[] = 'PARTITION BY ' . implode(', ', $expressions);
+ }
+
+ if ($this->order) {
+ $clauses[] = $this->order->sql($binder);
+ }
+
+ if ($this->frame) {
+ $start = $this->buildOffsetSql(
+ $binder,
+ $this->frame['start']['offset'],
+ $this->frame['start']['direction']
+ );
+ $end = $this->buildOffsetSql(
+ $binder,
+ $this->frame['end']['offset'],
+ $this->frame['end']['direction']
+ );
+
+ $frameSql = sprintf('%s BETWEEN %s AND %s', $this->frame['type'], $start, $end);
+
+ if ($this->exclusion !== null) {
+ $frameSql .= ' EXCLUDE ' . $this->exclusion;
+ }
+
+ $clauses[] = $frameSql;
+ }
+
+ return implode(' ', $clauses);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function traverse(Closure $callback)
+ {
+ $callback($this->name);
+ foreach ($this->partitions as $partition) {
+ $callback($partition);
+ $partition->traverse($callback);
+ }
+
+ if ($this->order) {
+ $callback($this->order);
+ $this->order->traverse($callback);
+ }
+
+ if ($this->frame !== null) {
+ $offset = $this->frame['start']['offset'];
+ if ($offset instanceof ExpressionInterface) {
+ $callback($offset);
+ $offset->traverse($callback);
+ }
+ $offset = $this->frame['end']['offset'] ?? null;
+ if ($offset instanceof ExpressionInterface) {
+ $callback($offset);
+ $offset->traverse($callback);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Builds frame offset sql.
+ *
+ * @param \Cake\Database\ValueBinder $binder Value binder
+ * @param int|string|\Cake\Database\ExpressionInterface|null $offset Frame offset
+ * @param string $direction Frame offset direction
+ * @return string
+ */
+ protected function buildOffsetSql(ValueBinder $binder, $offset, string $direction): string
+ {
+ if ($offset === 0) {
+ return 'CURRENT ROW';
+ }
+
+ if ($offset instanceof ExpressionInterface) {
+ $offset = $offset->sql($binder);
+ }
+
+ $sql = sprintf(
+ '%s %s',
+ $offset ?? 'UNBOUNDED',
+ $direction
+ );
+
+ return $sql;
+ }
+
+ /**
+ * Clone this object and its subtree of expressions.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ $this->name = clone $this->name;
+ foreach ($this->partitions as $i => $partition) {
+ $this->partitions[$i] = clone $partition;
+ }
+ if ($this->order !== null) {
+ $this->order = clone $this->order;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Expression/WindowInterface.php b/app/vendor/cakephp/cakephp/src/Database/Expression/WindowInterface.php
new file mode 100644
index 000000000..03e117ac8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Expression/WindowInterface.php
@@ -0,0 +1,163 @@
+_driver = $driver;
+ $map = $typeMap->toArray();
+ $types = TypeFactory::buildAll();
+
+ $simpleMap = $batchingMap = [];
+ $simpleResult = $batchingResult = [];
+
+ foreach ($types as $k => $type) {
+ if ($type instanceof OptionalConvertInterface && !$type->requiresToPhpCast()) {
+ continue;
+ }
+
+ if ($type instanceof BatchCastingInterface) {
+ $batchingMap[$k] = $type;
+ continue;
+ }
+
+ $simpleMap[$k] = $type;
+ }
+
+ foreach ($map as $field => $type) {
+ if (isset($simpleMap[$type])) {
+ $simpleResult[$field] = $simpleMap[$type];
+ continue;
+ }
+ if (isset($batchingMap[$type])) {
+ $batchingResult[$type][] = $field;
+ }
+ }
+
+ // Using batching when there is only a couple for the type is actually slower,
+ // so, let's check for that case here.
+ foreach ($batchingResult as $type => $fields) {
+ if (count($fields) > 2) {
+ continue;
+ }
+
+ foreach ($fields as $f) {
+ $simpleResult[$f] = $batchingMap[$type];
+ }
+ unset($batchingResult[$type]);
+ }
+
+ $this->types = $types;
+ $this->_typeMap = $simpleResult;
+ $this->batchingTypeMap = $batchingResult;
+ }
+
+ /**
+ * Converts each of the fields in the array that are present in the type map
+ * using the corresponding Type class.
+ *
+ * @param array $row The array with the fields to be casted
+ * @return array
+ */
+ public function __invoke(array $row): array
+ {
+ if (!empty($this->_typeMap)) {
+ foreach ($this->_typeMap as $field => $type) {
+ $row[$field] = $type->toPHP($row[$field], $this->_driver);
+ }
+ }
+
+ if (!empty($this->batchingTypeMap)) {
+ foreach ($this->batchingTypeMap as $t => $fields) {
+ /** @psalm-suppress PossiblyUndefinedMethod */
+ $row = $this->types[$t]->manyToPHP($row, $fields, $this->_driver);
+ }
+ }
+
+ return $row;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/FunctionsBuilder.php b/app/vendor/cakephp/cakephp/src/Database/FunctionsBuilder.php
new file mode 100644
index 000000000..5a5c0f297
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/FunctionsBuilder.php
@@ -0,0 +1,375 @@
+aggregate('SUM', $this->toLiteralParam($expression), $types, $returnType);
+ }
+
+ /**
+ * Returns a AggregateExpression representing a call to SQL AVG function.
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression the function argument
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function avg($expression, $types = []): AggregateExpression
+ {
+ return $this->aggregate('AVG', $this->toLiteralParam($expression), $types, 'float');
+ }
+
+ /**
+ * Returns a AggregateExpression representing a call to SQL MAX function.
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression the function argument
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function max($expression, $types = []): AggregateExpression
+ {
+ return $this->aggregate('MAX', $this->toLiteralParam($expression), $types, current($types) ?: 'float');
+ }
+
+ /**
+ * Returns a AggregateExpression representing a call to SQL MIN function.
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression the function argument
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function min($expression, $types = []): AggregateExpression
+ {
+ return $this->aggregate('MIN', $this->toLiteralParam($expression), $types, current($types) ?: 'float');
+ }
+
+ /**
+ * Returns a AggregateExpression representing a call to SQL COUNT function.
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression the function argument
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function count($expression, $types = []): AggregateExpression
+ {
+ return $this->aggregate('COUNT', $this->toLiteralParam($expression), $types, 'integer');
+ }
+
+ /**
+ * Returns a FunctionExpression representing a string concatenation
+ *
+ * @param array $args List of strings or expressions to concatenate
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function concat(array $args, array $types = []): FunctionExpression
+ {
+ return new FunctionExpression('CONCAT', $args, $types, 'string');
+ }
+
+ /**
+ * Returns a FunctionExpression representing a call to SQL COALESCE function.
+ *
+ * @param array $args List of expressions to evaluate as function parameters
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function coalesce(array $args, array $types = []): FunctionExpression
+ {
+ return new FunctionExpression('COALESCE', $args, $types, current($types) ?: 'string');
+ }
+
+ /**
+ * Returns a FunctionExpression representing a SQL CAST.
+ *
+ * The `$type` parameter is a SQL type. The return type for the returned expression
+ * is the default type name. Use `setReturnType()` to update it.
+ *
+ * @param string|\Cake\Database\ExpressionInterface $field Field or expression to cast.
+ * @param string $type The SQL data type
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function cast($field, string $type = ''): FunctionExpression
+ {
+ if (is_array($field)) {
+ deprecationWarning(
+ 'Build cast function by FunctionsBuilder::cast(array $args) is deprecated. ' .
+ 'Use FunctionsBuilder::cast($field, string $type) instead.'
+ );
+
+ return new FunctionExpression('CAST', $field);
+ }
+
+ if (empty($type)) {
+ throw new InvalidArgumentException('The `$type` in a cast cannot be empty.');
+ }
+
+ $expression = new FunctionExpression('CAST', $this->toLiteralParam($field));
+ $expression->setConjunction(' AS')->add([$type => 'literal']);
+
+ return $expression;
+ }
+
+ /**
+ * Returns a FunctionExpression representing the difference in days between
+ * two dates.
+ *
+ * @param array $args List of expressions to obtain the difference in days.
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function dateDiff(array $args, array $types = []): FunctionExpression
+ {
+ return new FunctionExpression('DATEDIFF', $args, $types, 'integer');
+ }
+
+ /**
+ * Returns the specified date part from the SQL expression.
+ *
+ * @param string $part Part of the date to return.
+ * @param string|\Cake\Database\ExpressionInterface $expression Expression to obtain the date part from.
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function datePart(string $part, $expression, array $types = []): FunctionExpression
+ {
+ return $this->extract($part, $expression, $types);
+ }
+
+ /**
+ * Returns the specified date part from the SQL expression.
+ *
+ * @param string $part Part of the date to return.
+ * @param string|\Cake\Database\ExpressionInterface $expression Expression to obtain the date part from.
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function extract(string $part, $expression, array $types = []): FunctionExpression
+ {
+ $expression = new FunctionExpression('EXTRACT', $this->toLiteralParam($expression), $types, 'integer');
+ $expression->setConjunction(' FROM')->add([$part => 'literal'], [], true);
+
+ return $expression;
+ }
+
+ /**
+ * Add the time unit to the date expression
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression Expression to obtain the date part from.
+ * @param string|int $value Value to be added. Use negative to subtract.
+ * @param string $unit Unit of the value e.g. hour or day.
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function dateAdd($expression, $value, string $unit, array $types = []): FunctionExpression
+ {
+ if (!is_numeric($value)) {
+ $value = 0;
+ }
+ $interval = $value . ' ' . $unit;
+ $expression = new FunctionExpression('DATE_ADD', $this->toLiteralParam($expression), $types, 'datetime');
+ $expression->setConjunction(', INTERVAL')->add([$interval => 'literal']);
+
+ return $expression;
+ }
+
+ /**
+ * Returns a FunctionExpression representing a call to SQL WEEKDAY function.
+ * 1 - Sunday, 2 - Monday, 3 - Tuesday...
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression the function argument
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function dayOfWeek($expression, $types = []): FunctionExpression
+ {
+ return new FunctionExpression('DAYOFWEEK', $this->toLiteralParam($expression), $types, 'integer');
+ }
+
+ /**
+ * Returns a FunctionExpression representing a call to SQL WEEKDAY function.
+ * 1 - Sunday, 2 - Monday, 3 - Tuesday...
+ *
+ * @param string|\Cake\Database\ExpressionInterface $expression the function argument
+ * @param array $types list of types to bind to the arguments
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function weekday($expression, $types = []): FunctionExpression
+ {
+ return $this->dayOfWeek($expression, $types);
+ }
+
+ /**
+ * Returns a FunctionExpression representing a call that will return the current
+ * date and time. By default it returns both date and time, but you can also
+ * make it generate only the date or only the time.
+ *
+ * @param string $type (datetime|date|time)
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function now(string $type = 'datetime'): FunctionExpression
+ {
+ if ($type === 'datetime') {
+ return new FunctionExpression('NOW', [], [], 'datetime');
+ }
+ if ($type === 'date') {
+ return new FunctionExpression('CURRENT_DATE', [], [], 'date');
+ }
+ if ($type === 'time') {
+ return new FunctionExpression('CURRENT_TIME', [], [], 'time');
+ }
+
+ throw new InvalidArgumentException('Invalid argument for FunctionsBuilder::now(): ' . $type);
+ }
+
+ /**
+ * Returns an AggregateExpression representing call to SQL ROW_NUMBER().
+ *
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function rowNumber(): AggregateExpression
+ {
+ return (new AggregateExpression('ROW_NUMBER', [], [], 'integer'))->over();
+ }
+
+ /**
+ * Returns an AggregateExpression representing call to SQL LAG().
+ *
+ * @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset
+ * @param int $offset The row offset
+ * @param mixed $default The default value if offset doesn't exist
+ * @param string $type The output type of the lag expression. Defaults to float.
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function lag($expression, int $offset, $default = null, $type = null): AggregateExpression
+ {
+ $params = $this->toLiteralParam($expression) + [$offset => 'literal'];
+ if ($default !== null) {
+ $params[] = $default;
+ }
+
+ $types = [];
+ if ($type !== null) {
+ $types = [$type, 'integer', $type];
+ }
+
+ return (new AggregateExpression('LAG', $params, $types, $type ?? 'float'))->over();
+ }
+
+ /**
+ * Returns an AggregateExpression representing call to SQL LEAD().
+ *
+ * @param \Cake\Database\ExpressionInterface|string $expression The value evaluated at offset
+ * @param int $offset The row offset
+ * @param mixed $default The default value if offset doesn't exist
+ * @param string $type The output type of the lead expression. Defaults to float.
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function lead($expression, int $offset, $default = null, $type = null): AggregateExpression
+ {
+ $params = $this->toLiteralParam($expression) + [$offset => 'literal'];
+ if ($default !== null) {
+ $params[] = $default;
+ }
+
+ $types = [];
+ if ($type !== null) {
+ $types = [$type, 'integer', $type];
+ }
+
+ return (new AggregateExpression('LEAD', $params, $types, $type ?? 'float'))->over();
+ }
+
+ /**
+ * Helper method to create arbitrary SQL aggregate function calls.
+ *
+ * @param string $name The SQL aggregate function name
+ * @param array $params Array of arguments to be passed to the function.
+ * Can be an associative array with the literal value or identifier:
+ * `['value' => 'literal']` or `['value' => 'identifier']
+ * @param array $types Array of types that match the names used in `$params`:
+ * `['name' => 'type']`
+ * @param string $return Return type of the entire expression. Defaults to float.
+ * @return \Cake\Database\Expression\AggregateExpression
+ */
+ public function aggregate(string $name, array $params = [], array $types = [], string $return = 'float')
+ {
+ return new AggregateExpression($name, $params, $types, $return);
+ }
+
+ /**
+ * Magic method dispatcher to create custom SQL function calls
+ *
+ * @param string $name the SQL function name to construct
+ * @param array $args list with up to 3 arguments, first one being an array with
+ * parameters for the SQL function, the second one a list of types to bind to those
+ * params, and the third one the return type of the function
+ * @return \Cake\Database\Expression\FunctionExpression
+ */
+ public function __call(string $name, array $args): FunctionExpression
+ {
+ return new FunctionExpression($name, ...$args);
+ }
+
+ /**
+ * Creates function parameter array from expression or string literal.
+ *
+ * @param \Cake\Database\ExpressionInterface|string $expression function argument
+ * @return (\Cake\Database\ExpressionInterface|string)[]
+ */
+ protected function toLiteralParam($expression)
+ {
+ if (is_string($expression)) {
+ return [$expression => 'literal'];
+ }
+
+ return [$expression];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/IdentifierQuoter.php b/app/vendor/cakephp/cakephp/src/Database/IdentifierQuoter.php
new file mode 100644
index 000000000..6b21a5ad6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/IdentifierQuoter.php
@@ -0,0 +1,269 @@
+_driver = $driver;
+ }
+
+ /**
+ * Iterates over each of the clauses in a query looking for identifiers and
+ * quotes them
+ *
+ * @param \Cake\Database\Query $query The query to have its identifiers quoted
+ * @return \Cake\Database\Query
+ */
+ public function quote(Query $query): Query
+ {
+ $binder = $query->getValueBinder();
+ $query->setValueBinder(null);
+
+ if ($query->type() === 'insert') {
+ $this->_quoteInsert($query);
+ } elseif ($query->type() === 'update') {
+ $this->_quoteUpdate($query);
+ } else {
+ $this->_quoteParts($query);
+ }
+
+ $query->traverseExpressions([$this, 'quoteExpression']);
+ $query->setValueBinder($binder);
+
+ return $query;
+ }
+
+ /**
+ * Quotes identifiers inside expression objects
+ *
+ * @param \Cake\Database\ExpressionInterface $expression The expression object to walk and quote.
+ * @return void
+ */
+ public function quoteExpression(ExpressionInterface $expression): void
+ {
+ if ($expression instanceof FieldInterface) {
+ $this->_quoteComparison($expression);
+
+ return;
+ }
+
+ if ($expression instanceof OrderByExpression) {
+ $this->_quoteOrderBy($expression);
+
+ return;
+ }
+
+ if ($expression instanceof IdentifierExpression) {
+ $this->_quoteIdentifierExpression($expression);
+
+ return;
+ }
+ }
+
+ /**
+ * Quotes all identifiers in each of the clauses of a query
+ *
+ * @param \Cake\Database\Query $query The query to quote.
+ * @return void
+ */
+ protected function _quoteParts(Query $query): void
+ {
+ foreach (['distinct', 'select', 'from', 'group'] as $part) {
+ $contents = $query->clause($part);
+
+ if (!is_array($contents)) {
+ continue;
+ }
+
+ $result = $this->_basicQuoter($contents);
+ if (!empty($result)) {
+ $query->{$part}($result, true);
+ }
+ }
+
+ $joins = $query->clause('join');
+ if ($joins) {
+ $joins = $this->_quoteJoins($joins);
+ $query->join($joins, [], true);
+ }
+ }
+
+ /**
+ * A generic identifier quoting function used for various parts of the query
+ *
+ * @param array $part the part of the query to quote
+ * @return array
+ */
+ protected function _basicQuoter(array $part): array
+ {
+ $result = [];
+ foreach ($part as $alias => $value) {
+ $value = !is_string($value) ? $value : $this->_driver->quoteIdentifier($value);
+ $alias = is_numeric($alias) ? $alias : $this->_driver->quoteIdentifier($alias);
+ $result[$alias] = $value;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Quotes both the table and alias for an array of joins as stored in a Query
+ * object
+ *
+ * @param array $joins The joins to quote.
+ * @return array
+ */
+ protected function _quoteJoins(array $joins): array
+ {
+ $result = [];
+ foreach ($joins as $value) {
+ $alias = '';
+ if (!empty($value['alias'])) {
+ $alias = $this->_driver->quoteIdentifier($value['alias']);
+ $value['alias'] = $alias;
+ }
+
+ if (is_string($value['table'])) {
+ $value['table'] = $this->_driver->quoteIdentifier($value['table']);
+ }
+
+ $result[$alias] = $value;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Quotes the table name and columns for an insert query
+ *
+ * @param \Cake\Database\Query $query The insert query to quote.
+ * @return void
+ */
+ protected function _quoteInsert(Query $query): void
+ {
+ $insert = $query->clause('insert');
+ if (!isset($insert[0]) || !isset($insert[1])) {
+ return;
+ }
+ [$table, $columns] = $insert;
+ $table = $this->_driver->quoteIdentifier($table);
+ foreach ($columns as &$column) {
+ if (is_scalar($column)) {
+ $column = $this->_driver->quoteIdentifier((string)$column);
+ }
+ }
+ $query->insert($columns)->into($table);
+ }
+
+ /**
+ * Quotes the table name for an update query
+ *
+ * @param \Cake\Database\Query $query The update query to quote.
+ * @return void
+ */
+ protected function _quoteUpdate(Query $query): void
+ {
+ $table = $query->clause('update')[0];
+
+ if (is_string($table)) {
+ $query->update($this->_driver->quoteIdentifier($table));
+ }
+ }
+
+ /**
+ * Quotes identifiers in expression objects implementing the field interface
+ *
+ * @param \Cake\Database\Expression\FieldInterface $expression The expression to quote.
+ * @return void
+ */
+ protected function _quoteComparison(FieldInterface $expression): void
+ {
+ $field = $expression->getField();
+ if (is_string($field)) {
+ $expression->setField($this->_driver->quoteIdentifier($field));
+ } elseif (is_array($field)) {
+ $quoted = [];
+ foreach ($field as $f) {
+ $quoted[] = $this->_driver->quoteIdentifier($f);
+ }
+ $expression->setField($quoted);
+ } elseif ($field instanceof ExpressionInterface) {
+ $this->quoteExpression($field);
+ }
+ }
+
+ /**
+ * Quotes identifiers in "order by" expression objects
+ *
+ * Strings with spaces are treated as literal expressions
+ * and will not have identifiers quoted.
+ *
+ * @param \Cake\Database\Expression\OrderByExpression $expression The expression to quote.
+ * @return void
+ */
+ protected function _quoteOrderBy(OrderByExpression $expression): void
+ {
+ $expression->iterateParts(function ($part, &$field) {
+ if (is_string($field)) {
+ $field = $this->_driver->quoteIdentifier($field);
+
+ return $part;
+ }
+ if (is_string($part) && strpos($part, ' ') === false) {
+ return $this->_driver->quoteIdentifier($part);
+ }
+
+ return $part;
+ });
+ }
+
+ /**
+ * Quotes identifiers in "order by" expression objects
+ *
+ * @param \Cake\Database\Expression\IdentifierExpression $expression The identifiers to quote.
+ * @return void
+ */
+ protected function _quoteIdentifierExpression(IdentifierExpression $expression): void
+ {
+ $expression->setIdentifier(
+ $this->_driver->quoteIdentifier($expression->getIdentifier())
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Database/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Database/Log/LoggedQuery.php b/app/vendor/cakephp/cakephp/src/Database/Log/LoggedQuery.php
new file mode 100644
index 000000000..bc014ee85
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Log/LoggedQuery.php
@@ -0,0 +1,162 @@
+ '\\$',
+ '\\' => '\\\\\\\\',
+ "'" => "''",
+ ];
+
+ $p = strtr($p, $replacements);
+
+ return "'$p'";
+ }
+
+ return $p;
+ }, $this->params);
+
+ $keys = [];
+ $limit = is_int(key($params)) ? 1 : -1;
+ foreach ($params as $key => $param) {
+ $keys[] = is_string($key) ? "/:$key\b/" : '/[?]/';
+ }
+
+ return preg_replace($keys, $params, $this->query, $limit);
+ }
+
+ /**
+ * Get the logging context data for a query.
+ *
+ * @return array
+ */
+ public function getContext(): array
+ {
+ return [
+ 'numRows' => $this->numRows,
+ 'took' => $this->took,
+ ];
+ }
+
+ /**
+ * Returns data that will be serialized as JSON
+ *
+ * @return array
+ */
+ public function jsonSerialize(): array
+ {
+ $error = $this->error;
+ if ($error !== null) {
+ $error = [
+ 'class' => get_class($error),
+ 'message' => $error->getMessage(),
+ 'code' => $error->getCode(),
+ ];
+ }
+
+ return [
+ 'query' => $this->query,
+ 'numRows' => $this->numRows,
+ 'params' => $this->params,
+ 'took' => $this->took,
+ 'error' => $error,
+ ];
+ }
+
+ /**
+ * Returns the string representation of this logged query
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ $sql = $this->query;
+ if (!empty($this->params)) {
+ $sql = $this->interpolate();
+ }
+
+ return $sql;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Log/LoggingStatement.php b/app/vendor/cakephp/cakephp/src/Database/Log/LoggingStatement.php
new file mode 100644
index 000000000..581a2cb0d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Log/LoggingStatement.php
@@ -0,0 +1,194 @@
+startTime = microtime(true);
+
+ $this->loggedQuery = new LoggedQuery();
+ $this->loggedQuery->params = $params ?: $this->_compiledParams;
+
+ try {
+ $result = parent::execute($params);
+ $this->loggedQuery->took = (int)round((microtime(true) - $this->startTime) * 1000, 0);
+ } catch (Exception $e) {
+ /** @psalm-suppress UndefinedPropertyAssignment */
+ $e->queryString = $this->queryString;
+ $this->loggedQuery->error = $e;
+ $this->_log();
+ throw $e;
+ }
+
+ if (preg_match('/^(?!SELECT)/i', $this->queryString)) {
+ $this->rowCount();
+ }
+
+ return $result;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetch($type = self::FETCH_TYPE_NUM)
+ {
+ $record = parent::fetch($type);
+
+ if ($this->loggedQuery) {
+ $this->rowCount();
+ }
+
+ return $record;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetchAll($type = self::FETCH_TYPE_NUM)
+ {
+ $results = parent::fetchAll($type);
+
+ if ($this->loggedQuery) {
+ $this->rowCount();
+ }
+
+ return $results;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function rowCount(): int
+ {
+ $result = parent::rowCount();
+
+ if ($this->loggedQuery) {
+ $this->loggedQuery->numRows = $result;
+ $this->_log();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Copies the logging data to the passed LoggedQuery and sends it
+ * to the logging system.
+ *
+ * @return void
+ */
+ protected function _log(): void
+ {
+ if ($this->loggedQuery === null) {
+ return;
+ }
+
+ $this->loggedQuery->query = $this->queryString;
+ $this->getLogger()->debug((string)$this->loggedQuery, ['query' => $this->loggedQuery]);
+
+ $this->loggedQuery = null;
+ }
+
+ /**
+ * Wrapper for bindValue function to gather each parameter to be later used
+ * in the logger function.
+ *
+ * @param string|int $column Name or param position to be bound
+ * @param mixed $value The value to bind to variable in query
+ * @param string|int|null $type PDO type or name of configured Type class
+ * @return void
+ */
+ public function bindValue($column, $value, $type = 'string'): void
+ {
+ parent::bindValue($column, $value, $type);
+
+ if ($type === null) {
+ $type = 'string';
+ }
+ if (!ctype_digit($type)) {
+ $value = $this->cast($value, $type)[0];
+ }
+ $this->_compiledParams[$column] = $value;
+ }
+
+ /**
+ * Sets a logger
+ *
+ * @param \Psr\Log\LoggerInterface $logger Logger object
+ * @return void
+ */
+ public function setLogger(LoggerInterface $logger): void
+ {
+ $this->_logger = $logger;
+ }
+
+ /**
+ * Gets the logger object
+ *
+ * @return \Psr\Log\LoggerInterface logger instance
+ */
+ public function getLogger(): LoggerInterface
+ {
+ return $this->_logger;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Log/QueryLogger.php b/app/vendor/cakephp/cakephp/src/Database/Log/QueryLogger.php
new file mode 100644
index 000000000..04040889c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Log/QueryLogger.php
@@ -0,0 +1,57 @@
+_defaultConfig['scopes'] = ['queriesLog'];
+ $this->_defaultConfig['connection'] = '';
+
+ parent::__construct($config);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function log($level, $message, array $context = [])
+ {
+ $context['scope'] = $this->scopes() ?: ['queriesLog'];
+ $context['connection'] = $this->getConfig('connection');
+
+ if ($context['query'] instanceof LoggedQuery) {
+ $context = $context['query']->getContext() + $context;
+ $message = 'connection={connection} duration={took} rows={numRows} ' . $message;
+ }
+ Log::write('debug', $message, $context);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/PostgresCompiler.php b/app/vendor/cakephp/cakephp/src/Database/PostgresCompiler.php
new file mode 100644
index 000000000..7beae8b58
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/PostgresCompiler.php
@@ -0,0 +1,93 @@
+ 'DELETE',
+ 'where' => ' WHERE %s',
+ 'group' => ' GROUP BY %s',
+ 'order' => ' %s',
+ 'limit' => ' LIMIT %s',
+ 'offset' => ' OFFSET %s',
+ 'epilog' => ' %s',
+ ];
+
+ /**
+ * Helper function used to build the string representation of a HAVING clause,
+ * it constructs the field list taking care of aliasing and
+ * converting expression objects to string.
+ *
+ * @param array $parts list of fields to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildHavingPart($parts, $query, $binder)
+ {
+ $selectParts = $query->clause('select');
+
+ foreach ($selectParts as $selectKey => $selectPart) {
+ if (!$selectPart instanceof FunctionExpression) {
+ continue;
+ }
+ foreach ($parts as $k => $p) {
+ if (!is_string($p)) {
+ continue;
+ }
+ preg_match_all(
+ '/\b' . trim($selectKey, '"') . '\b/i',
+ $p,
+ $matches
+ );
+
+ if (empty($matches[0])) {
+ continue;
+ }
+
+ $parts[$k] = preg_replace(
+ ['/"/', '/\b' . trim($selectKey, '"') . '\b/i'],
+ ['', $selectPart->sql($binder)],
+ $p
+ );
+ }
+ }
+
+ return sprintf(' HAVING %s', implode(', ', $parts));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Query.php b/app/vendor/cakephp/cakephp/src/Database/Query.php
new file mode 100644
index 000000000..030decaeb
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Query.php
@@ -0,0 +1,2386 @@
+ true,
+ 'update' => [],
+ 'set' => [],
+ 'insert' => [],
+ 'values' => [],
+ 'with' => [],
+ 'select' => [],
+ 'distinct' => false,
+ 'modifier' => [],
+ 'from' => [],
+ 'join' => [],
+ 'where' => null,
+ 'group' => [],
+ 'having' => null,
+ 'window' => [],
+ 'order' => null,
+ 'limit' => null,
+ 'offset' => null,
+ 'union' => [],
+ 'epilog' => null,
+ ];
+
+ /**
+ * The list of query clauses to traverse for generating a SELECT statement
+ *
+ * @var string[]
+ */
+ protected $_selectParts = [
+ 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit',
+ 'offset', 'union', 'epilog',
+ ];
+
+ /**
+ * The list of query clauses to traverse for generating an UPDATE statement
+ *
+ * @var string[]
+ */
+ protected $_updateParts = ['with', 'update', 'set', 'where', 'epilog'];
+
+ /**
+ * The list of query clauses to traverse for generating a DELETE statement
+ *
+ * @var string[]
+ */
+ protected $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog'];
+
+ /**
+ * The list of query clauses to traverse for generating an INSERT statement
+ *
+ * @var string[]
+ */
+ protected $_insertParts = ['with', 'insert', 'values', 'epilog'];
+
+ /**
+ * Indicates whether internal state of this query was changed, this is used to
+ * discard internal cached objects such as the transformed query or the reference
+ * to the executed statement.
+ *
+ * @var bool
+ */
+ protected $_dirty = false;
+
+ /**
+ * A list of callback functions to be called to alter each row from resulting
+ * statement upon retrieval. Each one of the callback function will receive
+ * the row array as first argument.
+ *
+ * @var callable[]
+ */
+ protected $_resultDecorators = [];
+
+ /**
+ * Statement object resulting from executing this query.
+ *
+ * @var \Cake\Database\StatementInterface|null
+ */
+ protected $_iterator;
+
+ /**
+ * The object responsible for generating query placeholders and temporarily store values
+ * associated to each of those.
+ *
+ * @var \Cake\Database\ValueBinder|null
+ */
+ protected $_valueBinder;
+
+ /**
+ * Instance of functions builder object used for generating arbitrary SQL functions.
+ *
+ * @var \Cake\Database\FunctionsBuilder|null
+ */
+ protected $_functionsBuilder;
+
+ /**
+ * Boolean for tracking whether or not buffered results
+ * are enabled.
+ *
+ * @var bool
+ */
+ protected $_useBufferedResults = true;
+
+ /**
+ * The Type map for fields in the select clause
+ *
+ * @var \Cake\Database\TypeMap|null
+ */
+ protected $_selectTypeMap;
+
+ /**
+ * Tracking flag to disable casting
+ *
+ * @var bool
+ */
+ protected $typeCastEnabled = true;
+
+ /**
+ * Constructor.
+ *
+ * @param \Cake\Database\Connection $connection The connection
+ * object to be used for transforming and executing this query
+ */
+ public function __construct(Connection $connection)
+ {
+ $this->setConnection($connection);
+ }
+
+ /**
+ * Sets the connection instance to be used for executing and transforming this query.
+ *
+ * @param \Cake\Database\Connection $connection Connection instance
+ * @return $this
+ */
+ public function setConnection(Connection $connection)
+ {
+ $this->_dirty();
+ $this->_connection = $connection;
+
+ return $this;
+ }
+
+ /**
+ * Gets the connection instance to be used for executing and transforming this query.
+ *
+ * @return \Cake\Database\Connection
+ */
+ public function getConnection(): Connection
+ {
+ return $this->_connection;
+ }
+
+ /**
+ * Compiles the SQL representation of this query and executes it using the
+ * configured connection object. Returns the resulting statement object.
+ *
+ * Executing a query internally executes several steps, the first one is
+ * letting the connection transform this object to fit its particular dialect,
+ * this might result in generating a different Query object that will be the one
+ * to actually be executed. Immediately after, literal values are passed to the
+ * connection so they are bound to the query in a safe way. Finally, the resulting
+ * statement is decorated with custom objects to execute callbacks for each row
+ * retrieved if necessary.
+ *
+ * Resulting statement is traversable, so it can be used in any loop as you would
+ * with an array.
+ *
+ * This method can be overridden in query subclasses to decorate behavior
+ * around query execution.
+ *
+ * @return \Cake\Database\StatementInterface
+ */
+ public function execute(): StatementInterface
+ {
+ $statement = $this->_connection->run($this);
+ $this->_iterator = $this->_decorateStatement($statement);
+ $this->_dirty = false;
+
+ return $this->_iterator;
+ }
+
+ /**
+ * Executes the SQL of this query and immediately closes the statement before returning the row count of records
+ * changed.
+ *
+ * This method can be used with UPDATE and DELETE queries, but is not recommended for SELECT queries and is not
+ * used to count records.
+ *
+ * ## Example
+ *
+ * ```
+ * $rowCount = $query->update('articles')
+ * ->set(['published'=>true])
+ * ->where(['published'=>false])
+ * ->rowCountAndClose();
+ * ```
+ *
+ * The above example will change the published column to true for all false records, and return the number of
+ * records that were updated.
+ *
+ * @return int
+ */
+ public function rowCountAndClose(): int
+ {
+ $statement = $this->execute();
+ try {
+ return $statement->rowCount();
+ } finally {
+ $statement->closeCursor();
+ }
+ }
+
+ /**
+ * Returns the SQL representation of this object.
+ *
+ * This function will compile this query to make it compatible
+ * with the SQL dialect that is used by the connection, This process might
+ * add, remove or alter any query part or internal expression to make it
+ * executable in the target platform.
+ *
+ * The resulting query may have placeholders that will be replaced with the actual
+ * values when the query is executed, hence it is most suitable to use with
+ * prepared statements.
+ *
+ * @param \Cake\Database\ValueBinder|null $binder Value binder that generates parameter placeholders
+ * @return string
+ */
+ public function sql(?ValueBinder $binder = null): string
+ {
+ if (!$binder) {
+ $binder = $this->getValueBinder();
+ $binder->resetCount();
+ }
+
+ return $this->getConnection()->compileQuery($this, $binder);
+ }
+
+ /**
+ * Will iterate over every specified part. Traversing functions can aggregate
+ * results using variables in the closure or instance variables. This function
+ * is commonly used as a way for traversing all query parts that
+ * are going to be used for constructing a query.
+ *
+ * The callback will receive 2 parameters, the first one is the value of the query
+ * part that is being iterated and the second the name of such part.
+ *
+ * ### Example
+ * ```
+ * $query->select(['title'])->from('articles')->traverse(function ($value, $clause) {
+ * if ($clause === 'select') {
+ * var_dump($value);
+ * }
+ * });
+ * ```
+ *
+ * @param callable $callback A function or callable to be executed for each part
+ * @return $this
+ */
+ public function traverse($callback)
+ {
+ foreach ($this->_parts as $name => $part) {
+ $callback($part, $name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Will iterate over the provided parts.
+ *
+ * Traversing functions can aggregate results using variables in the closure
+ * or instance variables. This method can be used to traverse a subset of
+ * query parts in order to render a SQL query.
+ *
+ * The callback will receive 2 parameters, the first one is the value of the query
+ * part that is being iterated and the second the name of such part.
+ *
+ * ### Example
+ *
+ * ```
+ * $query->select(['title'])->from('articles')->traverse(function ($value, $clause) {
+ * if ($clause === 'select') {
+ * var_dump($value);
+ * }
+ * }, ['select', 'from']);
+ * ```
+ *
+ * @param callable $visitor A function or callable to be executed for each part
+ * @param string[] $parts The list of query parts to traverse
+ * @return $this
+ */
+ public function traverseParts(callable $visitor, array $parts)
+ {
+ foreach ($parts as $name) {
+ $visitor($this->_parts[$name], $name);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds a new common table expression (CTE) to the query.
+ *
+ * ### Examples:
+ *
+ * Common table expressions can either be passed as preconstructed expression
+ * objects:
+ *
+ * ```
+ * $cte = new \Cake\Database\Expression\CommonTableExpression(
+ * 'cte',
+ * $connection
+ * ->newQuery()
+ * ->select('*')
+ * ->from('articles')
+ * );
+ *
+ * $query->with($cte);
+ * ```
+ *
+ * or returned from a closure, which will receive a new common table expression
+ * object as the first argument, and a new blank query object as
+ * the second argument:
+ *
+ * ```
+ * $query->with(function (
+ * \Cake\Database\Expression\CommonTableExpression $cte,
+ * \Cake\Database\Query $query
+ * ) {
+ * $cteQuery = $query
+ * ->select('*')
+ * ->from('articles');
+ *
+ * return $cte
+ * ->name('cte')
+ * ->query($cteQuery);
+ * });
+ * ```
+ *
+ * @param \Closure|\Cake\Database\Expression\CommonTableExpression $cte The CTE to add.
+ * @param bool $overwrite Whether to reset the list of CTEs.
+ * @return $this
+ */
+ public function with($cte, bool $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['with'] = [];
+ }
+
+ if ($cte instanceof Closure) {
+ $query = $this->getConnection()->newQuery();
+ $cte = $cte(new CommonTableExpression(), $query);
+ if (!($cte instanceof CommonTableExpression)) {
+ throw new RuntimeException(
+ 'You must return a `CommonTableExpression` from a Closure passed to `with()`.'
+ );
+ }
+ }
+
+ $this->_parts['with'][] = $cte;
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds new fields to be returned by a `SELECT` statement when this query is
+ * executed. Fields can be passed as an array of strings, array of expression
+ * objects, a single expression or a single string.
+ *
+ * If an array is passed, keys will be used to alias fields using the value as the
+ * real field to be aliased. It is possible to alias strings, Expression objects or
+ * even other Query objects.
+ *
+ * If a callable function is passed, the returning array of the function will
+ * be used as the list of fields.
+ *
+ * By default this function will append any passed argument to the list of fields
+ * to be selected, unless the second argument is set to true.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $query->select(['id', 'title']); // Produces SELECT id, title
+ * $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author
+ * $query->select('id', true); // Resets the list: SELECT id
+ * $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total
+ * $query->select(function ($query) {
+ * return ['article_id', 'total' => $query->count('*')];
+ * })
+ * ```
+ *
+ * By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append
+ * fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields
+ * from the table.
+ *
+ * @param array|\Cake\Database\ExpressionInterface|string|callable $fields fields to be added to the list.
+ * @param bool $overwrite whether to reset fields with passed list or not
+ * @return $this
+ */
+ public function select($fields = [], bool $overwrite = false)
+ {
+ if (!is_string($fields) && is_callable($fields)) {
+ $fields = $fields($this);
+ }
+
+ if (!is_array($fields)) {
+ $fields = [$fields];
+ }
+
+ if ($overwrite) {
+ $this->_parts['select'] = $fields;
+ } else {
+ $this->_parts['select'] = array_merge($this->_parts['select'], $fields);
+ }
+
+ $this->_dirty();
+ $this->_type = 'select';
+
+ return $this;
+ }
+
+ /**
+ * Adds a `DISTINCT` clause to the query to remove duplicates from the result set.
+ * This clause can only be used for select statements.
+ *
+ * If you wish to filter duplicates based of those rows sharing a particular field
+ * or set of fields, you may pass an array of fields to filter on. Beware that
+ * this option might not be fully supported in all database systems.
+ *
+ * ### Examples:
+ *
+ * ```
+ * // Filters products with the same name and city
+ * $query->select(['name', 'city'])->from('products')->distinct();
+ *
+ * // Filters products in the same city
+ * $query->distinct(['city']);
+ * $query->distinct('city');
+ *
+ * // Filter products with the same name
+ * $query->distinct(['name'], true);
+ * $query->distinct('name', true);
+ * ```
+ *
+ * @param array|\Cake\Database\ExpressionInterface|string|bool $on Enable/disable distinct class
+ * or list of fields to be filtered on
+ * @param bool $overwrite whether to reset fields with passed list or not
+ * @return $this
+ */
+ public function distinct($on = [], $overwrite = false)
+ {
+ if ($on === []) {
+ $on = true;
+ } elseif (is_string($on)) {
+ $on = [$on];
+ }
+
+ if (is_array($on)) {
+ $merge = [];
+ if (is_array($this->_parts['distinct'])) {
+ $merge = $this->_parts['distinct'];
+ }
+ $on = $overwrite ? array_values($on) : array_merge($merge, array_values($on));
+ }
+
+ $this->_parts['distinct'] = $on;
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds a single or multiple `SELECT` modifiers to be used in the `SELECT`.
+ *
+ * By default this function will append any passed argument to the list of modifiers
+ * to be applied, unless the second argument is set to true.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Ignore cache query in MySQL
+ * $query->select(['name', 'city'])->from('products')->modifier('SQL_NO_CACHE');
+ * // It will produce the SQL: SELECT SQL_NO_CACHE name, city FROM products
+ *
+ * // Or with multiple modifiers
+ * $query->select(['name', 'city'])->from('products')->modifier(['HIGH_PRIORITY', 'SQL_NO_CACHE']);
+ * // It will produce the SQL: SELECT HIGH_PRIORITY SQL_NO_CACHE name, city FROM products
+ * ```
+ *
+ * @param array|\Cake\Database\ExpressionInterface|string $modifiers modifiers to be applied to the query
+ * @param bool $overwrite whether to reset order with field list or not
+ * @return $this
+ */
+ public function modifier($modifiers, $overwrite = false)
+ {
+ $this->_dirty();
+ if ($overwrite) {
+ $this->_parts['modifier'] = [];
+ }
+ $this->_parts['modifier'] = array_merge($this->_parts['modifier'], (array)$modifiers);
+
+ return $this;
+ }
+
+ /**
+ * Adds a single or multiple tables to be used in the FROM clause for this query.
+ * Tables can be passed as an array of strings, array of expression
+ * objects, a single expression or a single string.
+ *
+ * If an array is passed, keys will be used to alias tables using the value as the
+ * real field to be aliased. It is possible to alias strings, ExpressionInterface objects or
+ * even other Query objects.
+ *
+ * By default this function will append any passed argument to the list of tables
+ * to be selected from, unless the second argument is set to true.
+ *
+ * This method can be used for select, update and delete statements.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $query->from(['p' => 'posts']); // Produces FROM posts p
+ * $query->from('authors'); // Appends authors: FROM posts p, authors
+ * $query->from(['products'], true); // Resets the list: FROM products
+ * $query->from(['sub' => $countQuery]); // FROM (SELECT ...) sub
+ * ```
+ *
+ * @param array|string $tables tables to be added to the list. This argument, can be
+ * passed as an array of strings, array of expression objects, or a single string. See
+ * the examples above for the valid call types.
+ * @param bool $overwrite whether to reset tables with passed list or not
+ * @return $this
+ */
+ public function from($tables = [], $overwrite = false)
+ {
+ $tables = (array)$tables;
+
+ if ($overwrite) {
+ $this->_parts['from'] = $tables;
+ } else {
+ $this->_parts['from'] = array_merge($this->_parts['from'], $tables);
+ }
+
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds a single or multiple tables to be used as JOIN clauses to this query.
+ * Tables can be passed as an array of strings, an array describing the
+ * join parts, an array with multiple join descriptions, or a single string.
+ *
+ * By default this function will append any passed argument to the list of tables
+ * to be joined, unless the third argument is set to true.
+ *
+ * When no join type is specified an `INNER JOIN` is used by default:
+ * `$query->join(['authors'])` will produce `INNER JOIN authors ON 1 = 1`
+ *
+ * It is also possible to alias joins using the array key:
+ * `$query->join(['a' => 'authors'])` will produce `INNER JOIN authors a ON 1 = 1`
+ *
+ * A join can be fully described and aliased using the array notation:
+ *
+ * ```
+ * $query->join([
+ * 'a' => [
+ * 'table' => 'authors',
+ * 'type' => 'LEFT',
+ * 'conditions' => 'a.id = b.author_id'
+ * ]
+ * ]);
+ * // Produces LEFT JOIN authors a ON a.id = b.author_id
+ * ```
+ *
+ * You can even specify multiple joins in an array, including the full description:
+ *
+ * ```
+ * $query->join([
+ * 'a' => [
+ * 'table' => 'authors',
+ * 'type' => 'LEFT',
+ * 'conditions' => 'a.id = b.author_id'
+ * ],
+ * 'p' => [
+ * 'table' => 'publishers',
+ * 'type' => 'INNER',
+ * 'conditions' => 'p.id = b.publisher_id AND p.name = "Cake Software Foundation"'
+ * ]
+ * ]);
+ * // LEFT JOIN authors a ON a.id = b.author_id
+ * // INNER JOIN publishers p ON p.id = b.publisher_id AND p.name = "Cake Software Foundation"
+ * ```
+ *
+ * ### Using conditions and types
+ *
+ * Conditions can be expressed, as in the examples above, using a string for comparing
+ * columns, or string with already quoted literal values. Additionally it is
+ * possible to use conditions expressed in arrays or expression objects.
+ *
+ * When using arrays for expressing conditions, it is often desirable to convert
+ * the literal values to the correct database representation. This is achieved
+ * using the second parameter of this function.
+ *
+ * ```
+ * $query->join(['a' => [
+ * 'table' => 'articles',
+ * 'conditions' => [
+ * 'a.posted >=' => new DateTime('-3 days'),
+ * 'a.published' => true,
+ * 'a.author_id = authors.id'
+ * ]
+ * ]], ['a.posted' => 'datetime', 'a.published' => 'boolean'])
+ * ```
+ *
+ * ### Overwriting joins
+ *
+ * When creating aliased joins using the array notation, you can override
+ * previous join definitions by using the same alias in consequent
+ * calls to this function or you can replace all previously defined joins
+ * with another list if the third parameter for this function is set to true.
+ *
+ * ```
+ * $query->join(['alias' => 'table']); // joins table with as alias
+ * $query->join(['alias' => 'another_table']); // joins another_table with as alias
+ * $query->join(['something' => 'different_table'], [], true); // resets joins list
+ * ```
+ *
+ * @param array|string $tables list of tables to be joined in the query
+ * @param array $types associative array of type names used to bind values to query
+ * @param bool $overwrite whether to reset joins with passed list or not
+ * @see \Cake\Database\TypeFactory
+ * @return $this
+ */
+ public function join($tables, $types = [], $overwrite = false)
+ {
+ if (is_string($tables) || isset($tables['table'])) {
+ $tables = [$tables];
+ }
+
+ $joins = [];
+ $i = count($this->_parts['join']);
+ foreach ($tables as $alias => $t) {
+ if (!is_array($t)) {
+ $t = ['table' => $t, 'conditions' => $this->newExpr()];
+ }
+
+ if (!is_string($t['conditions']) && is_callable($t['conditions'])) {
+ $t['conditions'] = $t['conditions']($this->newExpr(), $this);
+ }
+
+ if (!($t['conditions'] instanceof ExpressionInterface)) {
+ $t['conditions'] = $this->newExpr()->add($t['conditions'], $types);
+ }
+ $alias = is_string($alias) ? $alias : null;
+ $joins[$alias ?: $i++] = $t + ['type' => static::JOIN_TYPE_INNER, 'alias' => $alias];
+ }
+
+ if ($overwrite) {
+ $this->_parts['join'] = $joins;
+ } else {
+ $this->_parts['join'] = array_merge($this->_parts['join'], $joins);
+ }
+
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Remove a join if it has been defined.
+ *
+ * Useful when you are redefining joins or want to re-order
+ * the join clauses.
+ *
+ * @param string $name The alias/name of the join to remove.
+ * @return $this
+ */
+ public function removeJoin(string $name)
+ {
+ unset($this->_parts['join'][$name]);
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds a single `LEFT JOIN` clause to the query.
+ *
+ * This is a shorthand method for building joins via `join()`.
+ *
+ * The table name can be passed as a string, or as an array in case it needs to
+ * be aliased:
+ *
+ * ```
+ * // LEFT JOIN authors ON authors.id = posts.author_id
+ * $query->leftJoin('authors', 'authors.id = posts.author_id');
+ *
+ * // LEFT JOIN authors a ON a.id = posts.author_id
+ * $query->leftJoin(['a' => 'authors'], 'a.id = posts.author_id');
+ * ```
+ *
+ * Conditions can be passed as strings, arrays, or expression objects. When
+ * using arrays it is possible to combine them with the `$types` parameter
+ * in order to define how to convert the values:
+ *
+ * ```
+ * $query->leftJoin(['a' => 'articles'], [
+ * 'a.posted >=' => new DateTime('-3 days'),
+ * 'a.published' => true,
+ * 'a.author_id = authors.id'
+ * ], ['a.posted' => 'datetime', 'a.published' => 'boolean']);
+ * ```
+ *
+ * See `join()` for further details on conditions and types.
+ *
+ * @param string|string[] $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param array $types a list of types associated to the conditions used for converting
+ * values to the corresponding database representation.
+ * @return $this
+ */
+ public function leftJoin($table, $conditions = [], $types = [])
+ {
+ $this->join($this->_makeJoin($table, $conditions, static::JOIN_TYPE_LEFT), $types);
+
+ return $this;
+ }
+
+ /**
+ * Adds a single `RIGHT JOIN` clause to the query.
+ *
+ * This is a shorthand method for building joins via `join()`.
+ *
+ * The arguments of this method are identical to the `leftJoin()` shorthand, please refer
+ * to that methods description for further details.
+ *
+ * @param string|string[] $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param array $types a list of types associated to the conditions used for converting
+ * values to the corresponding database representation.
+ * @return $this
+ */
+ public function rightJoin($table, $conditions = [], $types = [])
+ {
+ $this->join($this->_makeJoin($table, $conditions, static::JOIN_TYPE_RIGHT), $types);
+
+ return $this;
+ }
+
+ /**
+ * Adds a single `INNER JOIN` clause to the query.
+ *
+ * This is a shorthand method for building joins via `join()`.
+ *
+ * The arguments of this method are identical to the `leftJoin()` shorthand, please refer
+ * to that methods description for further details.
+ *
+ * @param string|array $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param array $types a list of types associated to the conditions used for converting
+ * values to the corresponding database representation.
+ * @return $this
+ */
+ public function innerJoin($table, $conditions = [], $types = [])
+ {
+ $this->join($this->_makeJoin($table, $conditions, static::JOIN_TYPE_INNER), $types);
+
+ return $this;
+ }
+
+ /**
+ * Returns an array that can be passed to the join method describing a single join clause
+ *
+ * @param string|string[] $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param string $type the join type to use
+ * @return array
+ * @psalm-suppress InvalidReturnType
+ */
+ protected function _makeJoin($table, $conditions, $type): array
+ {
+ $alias = $table;
+
+ if (is_array($table)) {
+ $alias = key($table);
+ $table = current($table);
+ }
+
+ /**
+ * @psalm-suppress InvalidArrayOffset
+ * @psalm-suppress InvalidReturnStatement
+ */
+ return [
+ $alias => [
+ 'table' => $table,
+ 'conditions' => $conditions,
+ 'type' => $type,
+ ],
+ ];
+ }
+
+ /**
+ * Adds a condition or set of conditions to be used in the WHERE clause for this
+ * query. Conditions can be expressed as an array of fields as keys with
+ * comparison operators in it, the values for the array will be used for comparing
+ * the field to such literal. Finally, conditions can be expressed as a single
+ * string or an array of strings.
+ *
+ * When using arrays, each entry will be joined to the rest of the conditions using
+ * an `AND` operator. Consecutive calls to this function will also join the new
+ * conditions specified using the AND operator. Additionally, values can be
+ * expressed using expression objects which can include other query objects.
+ *
+ * Any conditions created with this methods can be used with any `SELECT`, `UPDATE`
+ * and `DELETE` type of queries.
+ *
+ * ### Conditions using operators:
+ *
+ * ```
+ * $query->where([
+ * 'posted >=' => new DateTime('3 days ago'),
+ * 'title LIKE' => 'Hello W%',
+ * 'author_id' => 1,
+ * ], ['posted' => 'datetime']);
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1`
+ *
+ * Second parameter is used to specify what type is expected for each passed
+ * key. Valid types can be used from the mapped with Database\Type class.
+ *
+ * ### Nesting conditions with conjunctions:
+ *
+ * ```
+ * $query->where([
+ * 'author_id !=' => 1,
+ * 'OR' => ['published' => true, 'posted <' => new DateTime('now')],
+ * 'NOT' => ['title' => 'Hello']
+ * ], ['published' => boolean, 'posted' => 'datetime']
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')`
+ *
+ * You can nest conditions using conjunctions as much as you like. Sometimes, you
+ * may want to define 2 different options for the same key, in that case, you can
+ * wrap each condition inside a new array:
+ *
+ * `$query->where(['OR' => [['published' => false], ['published' => true]])`
+ *
+ * Would result in:
+ *
+ * `WHERE (published = false) OR (published = true)`
+ *
+ * Keep in mind that every time you call where() with the third param set to false
+ * (default), it will join the passed conditions to the previous stored list using
+ * the `AND` operator. Also, using the same array key twice in consecutive calls to
+ * this method will not override the previous value.
+ *
+ * ### Using expressions objects:
+ *
+ * ```
+ * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR');
+ * $query->where(['published' => true], ['published' => 'boolean'])->where($exp);
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE (id != 100 OR author_id != 1) AND published = 1`
+ *
+ * Other Query objects that be used as conditions for any field.
+ *
+ * ### Adding conditions in multiple steps:
+ *
+ * You can use callable functions to construct complex expressions, functions
+ * receive as first argument a new QueryExpression object and this query instance
+ * as second argument. Functions must return an expression object, that will be
+ * added the list of conditions for the query using the `AND` operator.
+ *
+ * ```
+ * $query
+ * ->where(['title !=' => 'Hello World'])
+ * ->where(function ($exp, $query) {
+ * $or = $exp->or(['id' => 1]);
+ * $and = $exp->and(['id >' => 2, 'id <' => 10]);
+ * return $or->add($and);
+ * });
+ * ```
+ *
+ * * The previous example produces:
+ *
+ * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))`
+ *
+ * ### Conditions as strings:
+ *
+ * ```
+ * $query->where(['articles.author_id = authors.id', 'modified IS NULL']);
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE articles.author_id = authors.id AND modified IS NULL`
+ *
+ * Please note that when using the array notation or the expression objects, all
+ * *values* will be correctly quoted and transformed to the correspondent database
+ * data type automatically for you, thus securing your application from SQL injections.
+ * The keys however, are not treated as unsafe input, and should be validated/sanitized.
+ *
+ * If you use string conditions make sure that your values are correctly quoted.
+ * The safest thing you can do is to never use string conditions.
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface|\Closure|null $conditions The conditions to filter on.
+ * @param array $types associative array of type names used to bind values to query
+ * @param bool $overwrite whether to reset conditions with passed list or not
+ * @see \Cake\Database\TypeFactory
+ * @see \Cake\Database\Expression\QueryExpression
+ * @return $this
+ */
+ public function where($conditions = null, array $types = [], bool $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['where'] = $this->newExpr();
+ }
+ $this->_conjugate('where', $conditions, 'AND', $types);
+
+ return $this;
+ }
+
+ /**
+ * Convenience method that adds a NOT NULL condition to the query
+ *
+ * @param array|string|\Cake\Database\ExpressionInterface $fields A single field or expressions or a list of them
+ * that should be not null.
+ * @return $this
+ */
+ public function whereNotNull($fields)
+ {
+ if (!is_array($fields)) {
+ $fields = [$fields];
+ }
+
+ $exp = $this->newExpr();
+
+ foreach ($fields as $field) {
+ $exp->isNotNull($field);
+ }
+
+ return $this->where($exp);
+ }
+
+ /**
+ * Convenience method that adds a IS NULL condition to the query
+ *
+ * @param array|string|\Cake\Database\ExpressionInterface $fields A single field or expressions or a list of them
+ * that should be null.
+ * @return $this
+ */
+ public function whereNull($fields)
+ {
+ if (!is_array($fields)) {
+ $fields = [$fields];
+ }
+
+ $exp = $this->newExpr();
+
+ foreach ($fields as $field) {
+ $exp->isNull($field);
+ }
+
+ return $this->where($exp);
+ }
+
+ /**
+ * Adds an IN condition or set of conditions to be used in the WHERE clause for this
+ * query.
+ *
+ * This method does allow empty inputs in contrast to where() if you set
+ * 'allowEmpty' to true.
+ * Be careful about using it without proper sanity checks.
+ *
+ * Options:
+ *
+ * - `types` - Associative array of type names used to bind values to query
+ * - `allowEmpty` - Allow empty array.
+ *
+ * @param string $field Field
+ * @param array $values Array of values
+ * @param array $options Options
+ * @return $this
+ */
+ public function whereInList(string $field, array $values, array $options = [])
+ {
+ $options += [
+ 'types' => [],
+ 'allowEmpty' => false,
+ ];
+
+ if ($options['allowEmpty'] && !$values) {
+ return $this->where('1=0');
+ }
+
+ return $this->where([$field . ' IN' => $values], $options['types']);
+ }
+
+ /**
+ * Adds a NOT IN condition or set of conditions to be used in the WHERE clause for this
+ * query.
+ *
+ * This method does allow empty inputs in contrast to where() if you set
+ * 'allowEmpty' to true.
+ * Be careful about using it without proper sanity checks.
+ *
+ * @param string $field Field
+ * @param array $values Array of values
+ * @param array $options Options
+ * @return $this
+ */
+ public function whereNotInList(string $field, array $values, array $options = [])
+ {
+ $options += [
+ 'types' => [],
+ 'allowEmpty' => false,
+ ];
+
+ if ($options['allowEmpty'] && !$values) {
+ return $this->where([$field . ' IS NOT' => null]);
+ }
+
+ return $this->where([$field . ' NOT IN' => $values], $options['types']);
+ }
+
+ /**
+ * Connects any previously defined set of conditions to the provided list
+ * using the AND operator. This function accepts the conditions list in the same
+ * format as the method `where` does, hence you can use arrays, expression objects
+ * callback functions or strings.
+ *
+ * It is important to notice that when calling this function, any previous set
+ * of conditions defined for this query will be treated as a single argument for
+ * the AND operator. This function will not only operate the most recently defined
+ * condition, but all the conditions as a whole.
+ *
+ * When using an array for defining conditions, creating constraints form each
+ * array entry will use the same logic as with the `where()` function. This means
+ * that each array entry will be joined to the other using the AND operator, unless
+ * you nest the conditions in the array using other operator.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $query->where(['title' => 'Hello World')->andWhere(['author_id' => 1]);
+ * ```
+ *
+ * Will produce:
+ *
+ * `WHERE title = 'Hello World' AND author_id = 1`
+ *
+ * ```
+ * $query
+ * ->where(['OR' => ['published' => false, 'published is NULL']])
+ * ->andWhere(['author_id' => 1, 'comments_count >' => 10])
+ * ```
+ *
+ * Produces:
+ *
+ * `WHERE (published = 0 OR published IS NULL) AND author_id = 1 AND comments_count > 10`
+ *
+ * ```
+ * $query
+ * ->where(['title' => 'Foo'])
+ * ->andWhere(function ($exp, $query) {
+ * return $exp
+ * ->or(['author_id' => 1])
+ * ->add(['author_id' => 2]);
+ * });
+ * ```
+ *
+ * Generates the following conditions:
+ *
+ * `WHERE (title = 'Foo') AND (author_id = 1 OR author_id = 2)`
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface|\Closure $conditions The conditions to add with AND.
+ * @param array $types associative array of type names used to bind values to query
+ * @see \Cake\Database\Query::where()
+ * @see \Cake\Database\TypeFactory
+ * @return $this
+ */
+ public function andWhere($conditions, array $types = [])
+ {
+ $this->_conjugate('where', $conditions, 'AND', $types);
+
+ return $this;
+ }
+
+ /**
+ * Adds a single or multiple fields to be used in the ORDER clause for this query.
+ * Fields can be passed as an array of strings, array of expression
+ * objects, a single expression or a single string.
+ *
+ * If an array is passed, keys will be used as the field itself and the value will
+ * represent the order in which such field should be ordered. When called multiple
+ * times with the same fields as key, the last order definition will prevail over
+ * the others.
+ *
+ * By default this function will append any passed argument to the list of fields
+ * to be selected, unless the second argument is set to true.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $query->order(['title' => 'DESC', 'author_id' => 'ASC']);
+ * ```
+ *
+ * Produces:
+ *
+ * `ORDER BY title DESC, author_id ASC`
+ *
+ * ```
+ * $query
+ * ->order(['title' => $query->newExpr('DESC NULLS FIRST')])
+ * ->order('author_id');
+ * ```
+ *
+ * Will generate:
+ *
+ * `ORDER BY title DESC NULLS FIRST, author_id`
+ *
+ * ```
+ * $expression = $query->newExpr()->add(['id % 2 = 0']);
+ * $query->order($expression)->order(['title' => 'ASC']);
+ * ```
+ *
+ * and
+ *
+ * ```
+ * $query->order(function ($exp, $query) {
+ * return [$exp->add(['id % 2 = 0']), 'title' => 'ASC'];
+ * });
+ * ```
+ *
+ * Will both become:
+ *
+ * `ORDER BY (id %2 = 0), title ASC`
+ *
+ * Order fields/directions are not sanitized by the query builder.
+ * You should use an allowed list of fields/directions when passing
+ * in user-supplied data to `order()`.
+ *
+ * If you need to set complex expressions as order conditions, you
+ * should use `orderAsc()` or `orderDesc()`.
+ *
+ * @param array|\Cake\Database\ExpressionInterface|\Closure|string $fields fields to be added to the list
+ * @param bool $overwrite whether to reset order with field list or not
+ * @return $this
+ */
+ public function order($fields, $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['order'] = null;
+ }
+
+ if (!$fields) {
+ return $this;
+ }
+
+ if (!$this->_parts['order']) {
+ $this->_parts['order'] = new OrderByExpression();
+ }
+ $this->_conjugate('order', $fields, '', []);
+
+ return $this;
+ }
+
+ /**
+ * Add an ORDER BY clause with an ASC direction.
+ *
+ * This method allows you to set complex expressions
+ * as order conditions unlike order()
+ *
+ * Order fields are not suitable for use with user supplied data as they are
+ * not sanitized by the query builder.
+ *
+ * @param string|\Cake\Database\Expression\QueryExpression|\Closure $field The field to order on.
+ * @param bool $overwrite Whether or not to reset the order clauses.
+ * @return $this
+ */
+ public function orderAsc($field, $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['order'] = null;
+ }
+ if (!$field) {
+ return $this;
+ }
+
+ if ($field instanceof Closure) {
+ $field = $field($this->newExpr(), $this);
+ }
+
+ if (!$this->_parts['order']) {
+ $this->_parts['order'] = new OrderByExpression();
+ }
+ $this->_parts['order']->add(new OrderClauseExpression($field, 'ASC'));
+
+ return $this;
+ }
+
+ /**
+ * Add an ORDER BY clause with a DESC direction.
+ *
+ * This method allows you to set complex expressions
+ * as order conditions unlike order()
+ *
+ * Order fields are not suitable for use with user supplied data as they are
+ * not sanitized by the query builder.
+ *
+ * @param string|\Cake\Database\Expression\QueryExpression|\Closure $field The field to order on.
+ * @param bool $overwrite Whether or not to reset the order clauses.
+ * @return $this
+ */
+ public function orderDesc($field, $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['order'] = null;
+ }
+ if (!$field) {
+ return $this;
+ }
+
+ if ($field instanceof Closure) {
+ $field = $field($this->newExpr(), $this);
+ }
+
+ if (!$this->_parts['order']) {
+ $this->_parts['order'] = new OrderByExpression();
+ }
+ $this->_parts['order']->add(new OrderClauseExpression($field, 'DESC'));
+
+ return $this;
+ }
+
+ /**
+ * Adds a single or multiple fields to be used in the GROUP BY clause for this query.
+ * Fields can be passed as an array of strings, array of expression
+ * objects, a single expression or a single string.
+ *
+ * By default this function will append any passed argument to the list of fields
+ * to be grouped, unless the second argument is set to true.
+ *
+ * ### Examples:
+ *
+ * ```
+ * // Produces GROUP BY id, title
+ * $query->group(['id', 'title']);
+ *
+ * // Produces GROUP BY title
+ * $query->group('title');
+ * ```
+ *
+ * Group fields are not suitable for use with user supplied data as they are
+ * not sanitized by the query builder.
+ *
+ * @param array|\Cake\Database\ExpressionInterface|string $fields fields to be added to the list
+ * @param bool $overwrite whether to reset fields with passed list or not
+ * @return $this
+ */
+ public function group($fields, $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['group'] = [];
+ }
+
+ if (!is_array($fields)) {
+ $fields = [$fields];
+ }
+
+ $this->_parts['group'] = array_merge($this->_parts['group'], array_values($fields));
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds a condition or set of conditions to be used in the `HAVING` clause for this
+ * query. This method operates in exactly the same way as the method `where()`
+ * does. Please refer to its documentation for an insight on how to using each
+ * parameter.
+ *
+ * Having fields are not suitable for use with user supplied data as they are
+ * not sanitized by the query builder.
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface|\Closure|null $conditions The having conditions.
+ * @param array $types associative array of type names used to bind values to query
+ * @param bool $overwrite whether to reset conditions with passed list or not
+ * @see \Cake\Database\Query::where()
+ * @return $this
+ */
+ public function having($conditions = null, $types = [], $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['having'] = $this->newExpr();
+ }
+ $this->_conjugate('having', $conditions, 'AND', $types);
+
+ return $this;
+ }
+
+ /**
+ * Connects any previously defined set of conditions to the provided list
+ * using the AND operator in the HAVING clause. This method operates in exactly
+ * the same way as the method `andWhere()` does. Please refer to its
+ * documentation for an insight on how to using each parameter.
+ *
+ * Having fields are not suitable for use with user supplied data as they are
+ * not sanitized by the query builder.
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface|\Closure $conditions The AND conditions for HAVING.
+ * @param array $types associative array of type names used to bind values to query
+ * @see \Cake\Database\Query::andWhere()
+ * @return $this
+ */
+ public function andHaving($conditions, $types = [])
+ {
+ $this->_conjugate('having', $conditions, 'AND', $types);
+
+ return $this;
+ }
+
+ /**
+ * Adds a named window expression.
+ *
+ * You are responsible for adding windows in the order your database requires.
+ *
+ * @param string $name Window name
+ * @param \Cake\Database\Expression\WindowExpression|\Closure $window Window expression
+ * @param bool $overwrite Clear all previous query window expressions
+ * @return $this
+ */
+ public function window(string $name, $window, bool $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['window'] = [];
+ }
+
+ if ($window instanceof Closure) {
+ $window = $window(new WindowExpression(), $this);
+ if (!($window instanceof WindowExpression)) {
+ throw new RuntimeException('You must return a `WindowExpression` from a Closure passed to `window()`.');
+ }
+ }
+
+ $this->_parts['window'][] = ['name' => new IdentifierExpression($name), 'window' => $window];
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Set the page of results you want.
+ *
+ * This method provides an easier to use interface to set the limit + offset
+ * in the record set you want as results. If empty the limit will default to
+ * the existing limit clause, and if that too is empty, then `25` will be used.
+ *
+ * Pages must start at 1.
+ *
+ * @param int $num The page number you want.
+ * @param int|null $limit The number of rows you want in the page. If null
+ * the current limit clause will be used.
+ * @return $this
+ * @throws \InvalidArgumentException If page number < 1.
+ */
+ public function page(int $num, ?int $limit = null)
+ {
+ if ($num < 1) {
+ throw new InvalidArgumentException('Pages must start at 1.');
+ }
+ if ($limit !== null) {
+ $this->limit($limit);
+ }
+ $limit = $this->clause('limit');
+ if ($limit === null) {
+ $limit = 25;
+ $this->limit($limit);
+ }
+ $offset = ($num - 1) * $limit;
+ if (PHP_INT_MAX <= $offset) {
+ $offset = PHP_INT_MAX;
+ }
+ $this->offset((int)$offset);
+
+ return $this;
+ }
+
+ /**
+ * Sets the number of records that should be retrieved from database,
+ * accepts an integer or an expression object that evaluates to an integer.
+ * In some databases, this operation might not be supported or will require
+ * the query to be transformed in order to limit the result set size.
+ *
+ * ### Examples
+ *
+ * ```
+ * $query->limit(10) // generates LIMIT 10
+ * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1)
+ * ```
+ *
+ * @param int|\Cake\Database\ExpressionInterface|null $num number of records to be returned
+ * @return $this
+ */
+ public function limit($num)
+ {
+ $this->_dirty();
+ $this->_parts['limit'] = $num;
+
+ return $this;
+ }
+
+ /**
+ * Sets the number of records that should be skipped from the original result set
+ * This is commonly used for paginating large results. Accepts an integer or an
+ * expression object that evaluates to an integer.
+ *
+ * In some databases, this operation might not be supported or will require
+ * the query to be transformed in order to limit the result set size.
+ *
+ * ### Examples
+ *
+ * ```
+ * $query->offset(10) // generates OFFSET 10
+ * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1)
+ * ```
+ *
+ * @param int|\Cake\Database\ExpressionInterface|null $num number of records to be skipped
+ * @return $this
+ */
+ public function offset($num)
+ {
+ $this->_dirty();
+ $this->_parts['offset'] = $num;
+
+ return $this;
+ }
+
+ /**
+ * Adds a complete query to be used in conjunction with an UNION operator with
+ * this query. This is used to combine the result set of this query with the one
+ * that will be returned by the passed query. You can add as many queries as you
+ * required by calling multiple times this method with different queries.
+ *
+ * By default, the UNION operator will remove duplicate rows, if you wish to include
+ * every row for all queries, use unionAll().
+ *
+ * ### Examples
+ *
+ * ```
+ * $union = (new Query($conn))->select(['id', 'title'])->from(['a' => 'articles']);
+ * $query->select(['id', 'name'])->from(['d' => 'things'])->union($union);
+ * ```
+ *
+ * Will produce:
+ *
+ * `SELECT id, name FROM things d UNION SELECT id, title FROM articles a`
+ *
+ * @param string|\Cake\Database\Query $query full SQL query to be used in UNION operator
+ * @param bool $overwrite whether to reset the list of queries to be operated or not
+ * @return $this
+ */
+ public function union($query, $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['union'] = [];
+ }
+ $this->_parts['union'][] = [
+ 'all' => false,
+ 'query' => $query,
+ ];
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds a complete query to be used in conjunction with the UNION ALL operator with
+ * this query. This is used to combine the result set of this query with the one
+ * that will be returned by the passed query. You can add as many queries as you
+ * required by calling multiple times this method with different queries.
+ *
+ * Unlike UNION, UNION ALL will not remove duplicate rows.
+ *
+ * ```
+ * $union = (new Query($conn))->select(['id', 'title'])->from(['a' => 'articles']);
+ * $query->select(['id', 'name'])->from(['d' => 'things'])->unionAll($union);
+ * ```
+ *
+ * Will produce:
+ *
+ * `SELECT id, name FROM things d UNION ALL SELECT id, title FROM articles a`
+ *
+ * @param string|\Cake\Database\Query $query full SQL query to be used in UNION operator
+ * @param bool $overwrite whether to reset the list of queries to be operated or not
+ * @return $this
+ */
+ public function unionAll($query, $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_parts['union'] = [];
+ }
+ $this->_parts['union'][] = [
+ 'all' => true,
+ 'query' => $query,
+ ];
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Create an insert query.
+ *
+ * Note calling this method will reset any data previously set
+ * with Query::values().
+ *
+ * @param array $columns The columns to insert into.
+ * @param string[] $types A map between columns & their datatypes.
+ * @return $this
+ * @throws \RuntimeException When there are 0 columns.
+ */
+ public function insert(array $columns, array $types = [])
+ {
+ if (empty($columns)) {
+ throw new RuntimeException('At least 1 column is required to perform an insert.');
+ }
+ $this->_dirty();
+ $this->_type = 'insert';
+ $this->_parts['insert'][1] = $columns;
+ if (!$this->_parts['values']) {
+ $this->_parts['values'] = new ValuesExpression($columns, $this->getTypeMap()->setTypes($types));
+ } else {
+ $this->_parts['values']->setColumns($columns);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set the table name for insert queries.
+ *
+ * @param string $table The table name to insert into.
+ * @return $this
+ */
+ public function into(string $table)
+ {
+ $this->_dirty();
+ $this->_type = 'insert';
+ $this->_parts['insert'][0] = $table;
+
+ return $this;
+ }
+
+ /**
+ * Creates an expression that refers to an identifier. Identifiers are used to refer to field names and allow
+ * the SQL compiler to apply quotes or escape the identifier.
+ *
+ * The value is used as is, and you might be required to use aliases or include the table reference in
+ * the identifier. Do not use this method to inject SQL methods or logical statements.
+ *
+ * ### Example
+ *
+ * ```
+ * $query->newExpr()->lte('count', $query->identifier('total'));
+ * ```
+ *
+ * @param string $identifier The identifier for an expression
+ * @return \Cake\Database\ExpressionInterface
+ */
+ public function identifier(string $identifier): ExpressionInterface
+ {
+ return new IdentifierExpression($identifier);
+ }
+
+ /**
+ * Set the values for an insert query.
+ *
+ * Multi inserts can be performed by calling values() more than one time,
+ * or by providing an array of value sets. Additionally $data can be a Query
+ * instance to insert data from another SELECT statement.
+ *
+ * @param array|\Cake\Database\Query|\Cake\Database\Expression\ValuesExpression $data The data to insert.
+ * @return $this
+ * @throws \Cake\Database\Exception\DatabaseException if you try to set values before declaring columns.
+ * Or if you try to set values on non-insert queries.
+ */
+ public function values($data)
+ {
+ if ($this->_type !== 'insert') {
+ throw new DatabaseException(
+ 'You cannot add values before defining columns to use.'
+ );
+ }
+ if (empty($this->_parts['insert'])) {
+ throw new DatabaseException(
+ 'You cannot add values before defining columns to use.'
+ );
+ }
+
+ $this->_dirty();
+ if ($data instanceof ValuesExpression) {
+ $this->_parts['values'] = $data;
+
+ return $this;
+ }
+
+ $this->_parts['values']->add($data);
+
+ return $this;
+ }
+
+ /**
+ * Create an update query.
+ *
+ * Can be combined with set() and where() methods to create update queries.
+ *
+ * @param string|\Cake\Database\ExpressionInterface $table The table you want to update.
+ * @return $this
+ */
+ public function update($table)
+ {
+ if (!is_string($table) && !($table instanceof ExpressionInterface)) {
+ $text = 'Table must be of type string or "%s", got "%s"';
+ $message = sprintf($text, ExpressionInterface::class, gettype($table));
+ throw new InvalidArgumentException($message);
+ }
+
+ $this->_dirty();
+ $this->_type = 'update';
+ $this->_parts['update'][0] = $table;
+
+ return $this;
+ }
+
+ /**
+ * Set one or many fields to update.
+ *
+ * ### Examples
+ *
+ * Passing a string:
+ *
+ * ```
+ * $query->update('articles')->set('title', 'The Title');
+ * ```
+ *
+ * Passing an array:
+ *
+ * ```
+ * $query->update('articles')->set(['title' => 'The Title'], ['title' => 'string']);
+ * ```
+ *
+ * Passing a callable:
+ *
+ * ```
+ * $query->update('articles')->set(function ($exp) {
+ * return $exp->eq('title', 'The title', 'string');
+ * });
+ * ```
+ *
+ * @param string|array|\Closure|\Cake\Database\Expression\QueryExpression $key The column name or array of keys
+ * + values to set. This can also be a QueryExpression containing a SQL fragment.
+ * It can also be a Closure, that is required to return an expression object.
+ * @param mixed $value The value to update $key to. Can be null if $key is an
+ * array or QueryExpression. When $key is an array, this parameter will be
+ * used as $types instead.
+ * @param array|string $types The column types to treat data as.
+ * @return $this
+ */
+ public function set($key, $value = null, $types = [])
+ {
+ if (empty($this->_parts['set'])) {
+ $this->_parts['set'] = $this->newExpr()->setConjunction(',');
+ }
+
+ if ($key instanceof Closure) {
+ $exp = $this->newExpr()->setConjunction(',');
+ $this->_parts['set']->add($key($exp));
+
+ return $this;
+ }
+
+ if (is_array($key) || $key instanceof ExpressionInterface) {
+ $types = (array)$value;
+ $this->_parts['set']->add($key, $types);
+
+ return $this;
+ }
+
+ if (!is_string($types)) {
+ $types = null;
+ }
+ $this->_parts['set']->eq($key, $value, $types);
+
+ return $this;
+ }
+
+ /**
+ * Create a delete query.
+ *
+ * Can be combined with from(), where() and other methods to
+ * create delete queries with specific conditions.
+ *
+ * @param string|null $table The table to use when deleting.
+ * @return $this
+ */
+ public function delete(?string $table = null)
+ {
+ $this->_dirty();
+ $this->_type = 'delete';
+ if ($table !== null) {
+ $this->from($table);
+ }
+
+ return $this;
+ }
+
+ /**
+ * A string or expression that will be appended to the generated query
+ *
+ * ### Examples:
+ * ```
+ * $query->select('id')->where(['author_id' => 1])->epilog('FOR UPDATE');
+ * $query
+ * ->insert('articles', ['title'])
+ * ->values(['author_id' => 1])
+ * ->epilog('RETURNING id');
+ * ```
+ *
+ * Epliog content is raw SQL and not suitable for use with user supplied data.
+ *
+ * @param string|\Cake\Database\ExpressionInterface|null $expression The expression to be appended
+ * @return $this
+ */
+ public function epilog($expression = null)
+ {
+ $this->_dirty();
+ $this->_parts['epilog'] = $expression;
+
+ return $this;
+ }
+
+ /**
+ * Returns the type of this query (select, insert, update, delete)
+ *
+ * @return string
+ */
+ public function type(): string
+ {
+ return $this->_type;
+ }
+
+ /**
+ * Returns a new QueryExpression object. This is a handy function when
+ * building complex queries using a fluent interface. You can also override
+ * this function in subclasses to use a more specialized QueryExpression class
+ * if required.
+ *
+ * You can optionally pass a single raw SQL string or an array or expressions in
+ * any format accepted by \Cake\Database\Expression\QueryExpression:
+ *
+ * ```
+ * $expression = $query->newExpr(); // Returns an empty expression object
+ * $expression = $query->newExpr('Table.column = Table2.column'); // Return a raw SQL expression
+ * ```
+ *
+ * @param string|array|\Cake\Database\ExpressionInterface|null $rawExpression A string, array or anything you want wrapped in an expression object
+ * @return \Cake\Database\Expression\QueryExpression
+ */
+ public function newExpr($rawExpression = null): QueryExpression
+ {
+ $expression = new QueryExpression([], $this->getTypeMap());
+
+ if ($rawExpression !== null) {
+ $expression->add($rawExpression);
+ }
+
+ return $expression;
+ }
+
+ /**
+ * Returns an instance of a functions builder object that can be used for
+ * generating arbitrary SQL functions.
+ *
+ * ### Example:
+ *
+ * ```
+ * $query->func()->count('*');
+ * $query->func()->dateDiff(['2012-01-05', '2012-01-02'])
+ * ```
+ *
+ * @return \Cake\Database\FunctionsBuilder
+ */
+ public function func(): FunctionsBuilder
+ {
+ if ($this->_functionsBuilder === null) {
+ $this->_functionsBuilder = new FunctionsBuilder();
+ }
+
+ return $this->_functionsBuilder;
+ }
+
+ /**
+ * Executes this query and returns a results iterator. This function is required
+ * for implementing the IteratorAggregate interface and allows the query to be
+ * iterated without having to call execute() manually, thus making it look like
+ * a result set instead of the query itself.
+ *
+ * @return \Cake\Database\StatementInterface
+ * @psalm-suppress ImplementedReturnTypeMismatch
+ */
+ public function getIterator()
+ {
+ if ($this->_iterator === null || $this->_dirty) {
+ $this->_iterator = $this->execute();
+ }
+
+ return $this->_iterator;
+ }
+
+ /**
+ * Returns any data that was stored in the specified clause. This is useful for
+ * modifying any internal part of the query and it is used by the SQL dialects
+ * to transform the query accordingly before it is executed. The valid clauses that
+ * can be retrieved are: delete, update, set, insert, values, select, distinct,
+ * from, join, set, where, group, having, order, limit, offset and union.
+ *
+ * The return value for each of those parts may vary. Some clauses use QueryExpression
+ * to internally store their state, some use arrays and others may use booleans or
+ * integers. This is summary of the return types for each clause.
+ *
+ * - update: string The name of the table to update
+ * - set: QueryExpression
+ * - insert: array, will return an array containing the table + columns.
+ * - values: ValuesExpression
+ * - select: array, will return empty array when no fields are set
+ * - distinct: boolean
+ * - from: array of tables
+ * - join: array
+ * - set: array
+ * - where: QueryExpression, returns null when not set
+ * - group: array
+ * - having: QueryExpression, returns null when not set
+ * - order: OrderByExpression, returns null when not set
+ * - limit: integer or QueryExpression, null when not set
+ * - offset: integer or QueryExpression, null when not set
+ * - union: array
+ *
+ * @param string $name name of the clause to be returned
+ * @return mixed
+ * @throws \InvalidArgumentException When the named clause does not exist.
+ */
+ public function clause(string $name)
+ {
+ if (!array_key_exists($name, $this->_parts)) {
+ $clauses = implode(', ', array_keys($this->_parts));
+ throw new InvalidArgumentException("The '$name' clause is not defined. Valid clauses are: $clauses");
+ }
+
+ return $this->_parts[$name];
+ }
+
+ /**
+ * Registers a callback to be executed for each result that is fetched from the
+ * result set, the callback function will receive as first parameter an array with
+ * the raw data from the database for every row that is fetched and must return the
+ * row with any possible modifications.
+ *
+ * Callbacks will be executed lazily, if only 3 rows are fetched for database it will
+ * called 3 times, event though there might be more rows to be fetched in the cursor.
+ *
+ * Callbacks are stacked in the order they are registered, if you wish to reset the stack
+ * the call this function with the second parameter set to true.
+ *
+ * If you wish to remove all decorators from the stack, set the first parameter
+ * to null and the second to true.
+ *
+ * ### Example
+ *
+ * ```
+ * $query->decorateResults(function ($row) {
+ * $row['order_total'] = $row['subtotal'] + ($row['subtotal'] * $row['tax']);
+ * return $row;
+ * });
+ * ```
+ *
+ * @param callable|null $callback The callback to invoke when results are fetched.
+ * @param bool $overwrite Whether or not this should append or replace all existing decorators.
+ * @return $this
+ */
+ public function decorateResults(?callable $callback, bool $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_resultDecorators = [];
+ }
+
+ if ($callback !== null) {
+ $this->_resultDecorators[] = $callback;
+ }
+
+ return $this;
+ }
+
+ /**
+ * This function works similar to the traverse() function, with the difference
+ * that it does a full depth traversal of the entire expression tree. This will execute
+ * the provided callback function for each ExpressionInterface object that is
+ * stored inside this query at any nesting depth in any part of the query.
+ *
+ * Callback will receive as first parameter the currently visited expression.
+ *
+ * @param callable $callback the function to be executed for each ExpressionInterface
+ * found inside this query.
+ * @return $this
+ */
+ public function traverseExpressions(callable $callback)
+ {
+ if (!$callback instanceof Closure) {
+ $callback = Closure::fromCallable($callback);
+ }
+
+ foreach ($this->_parts as $part) {
+ $this->_expressionsVisitor($part, $callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Query parts traversal method used by traverseExpressions()
+ *
+ * @param \Cake\Database\ExpressionInterface|\Cake\Database\ExpressionInterface[] $expression Query expression or
+ * array of expressions.
+ * @param \Closure $callback The callback to be executed for each ExpressionInterface
+ * found inside this query.
+ * @return void
+ */
+ protected function _expressionsVisitor($expression, Closure $callback): void
+ {
+ if (is_array($expression)) {
+ foreach ($expression as $e) {
+ $this->_expressionsVisitor($e, $callback);
+ }
+
+ return;
+ }
+
+ if ($expression instanceof ExpressionInterface) {
+ $expression->traverse(function ($exp) use ($callback) {
+ $this->_expressionsVisitor($exp, $callback);
+ });
+
+ if (!$expression instanceof self) {
+ $callback($expression);
+ }
+ }
+ }
+
+ /**
+ * Associates a query placeholder to a value and a type.
+ *
+ * ```
+ * $query->bind(':id', 1, 'integer');
+ * ```
+ *
+ * @param string|int $param placeholder to be replaced with quoted version
+ * of $value
+ * @param mixed $value The value to be bound
+ * @param string|int|null $type the mapped type name, used for casting when sending
+ * to database
+ * @return $this
+ */
+ public function bind($param, $value, $type = null)
+ {
+ $this->getValueBinder()->bind($param, $value, $type);
+
+ return $this;
+ }
+
+ /**
+ * Returns the currently used ValueBinder instance.
+ *
+ * A ValueBinder is responsible for generating query placeholders and temporarily
+ * associate values to those placeholders so that they can be passed correctly
+ * to the statement object.
+ *
+ * @return \Cake\Database\ValueBinder
+ */
+ public function getValueBinder(): ValueBinder
+ {
+ if ($this->_valueBinder === null) {
+ $this->_valueBinder = new ValueBinder();
+ }
+
+ return $this->_valueBinder;
+ }
+
+ /**
+ * Overwrite the current value binder
+ *
+ * A ValueBinder is responsible for generating query placeholders and temporarily
+ * associate values to those placeholders so that they can be passed correctly
+ * to the statement object.
+ *
+ * @param \Cake\Database\ValueBinder|null $binder The binder or null to disable binding.
+ * @return $this
+ */
+ public function setValueBinder(?ValueBinder $binder)
+ {
+ $this->_valueBinder = $binder;
+
+ return $this;
+ }
+
+ /**
+ * Enables/Disables buffered results.
+ *
+ * When enabled the results returned by this Query will be
+ * buffered. This enables you to iterate a result set multiple times, or
+ * both cache and iterate it.
+ *
+ * When disabled it will consume less memory as fetched results are not
+ * remembered for future iterations.
+ *
+ * @param bool $enable Whether or not to enable buffering
+ * @return $this
+ */
+ public function enableBufferedResults(bool $enable = true)
+ {
+ $this->_dirty();
+ $this->_useBufferedResults = $enable;
+
+ return $this;
+ }
+
+ /**
+ * Disables buffered results.
+ *
+ * Disabling buffering will consume less memory as fetched results are not
+ * remembered for future iterations.
+ *
+ * @return $this
+ */
+ public function disableBufferedResults()
+ {
+ $this->_dirty();
+ $this->_useBufferedResults = false;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether buffered results are enabled/disabled.
+ *
+ * When enabled the results returned by this Query will be
+ * buffered. This enables you to iterate a result set multiple times, or
+ * both cache and iterate it.
+ *
+ * When disabled it will consume less memory as fetched results are not
+ * remembered for future iterations.
+ *
+ * @return bool
+ */
+ public function isBufferedResultsEnabled(): bool
+ {
+ return $this->_useBufferedResults;
+ }
+
+ /**
+ * Sets the TypeMap class where the types for each of the fields in the
+ * select clause are stored.
+ *
+ * @param \Cake\Database\TypeMap $typeMap The map object to use
+ * @return $this
+ */
+ public function setSelectTypeMap(TypeMap $typeMap)
+ {
+ $this->_selectTypeMap = $typeMap;
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Gets the TypeMap class where the types for each of the fields in the
+ * select clause are stored.
+ *
+ * @return \Cake\Database\TypeMap
+ */
+ public function getSelectTypeMap(): TypeMap
+ {
+ if ($this->_selectTypeMap === null) {
+ $this->_selectTypeMap = new TypeMap();
+ }
+
+ return $this->_selectTypeMap;
+ }
+
+ /**
+ * Disables result casting.
+ *
+ * When disabled, the fields will be returned as received from the database
+ * driver (which in most environments means they are being returned as
+ * strings), which can improve performance with larger datasets.
+ *
+ * @return $this
+ */
+ public function disableResultsCasting()
+ {
+ $this->typeCastEnabled = false;
+
+ return $this;
+ }
+
+ /**
+ * Enables result casting.
+ *
+ * When enabled, the fields in the results returned by this Query will be
+ * cast to their corresponding PHP data type.
+ *
+ * @return $this
+ */
+ public function enableResultsCasting()
+ {
+ $this->typeCastEnabled = true;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether result casting is enabled/disabled.
+ *
+ * When enabled, the fields in the results returned by this Query will be
+ * casted to their corresponding PHP data type.
+ *
+ * When disabled, the fields will be returned as received from the database
+ * driver (which in most environments means they are being returned as
+ * strings), which can improve performance with larger datasets.
+ *
+ * @return bool
+ */
+ public function isResultsCastingEnabled(): bool
+ {
+ return $this->typeCastEnabled;
+ }
+
+ /**
+ * Auxiliary function used to wrap the original statement from the driver with
+ * any registered callbacks.
+ *
+ * @param \Cake\Database\StatementInterface $statement to be decorated
+ * @return \Cake\Database\Statement\CallbackStatement|\Cake\Database\StatementInterface
+ */
+ protected function _decorateStatement(StatementInterface $statement)
+ {
+ $typeMap = $this->getSelectTypeMap();
+ $driver = $this->getConnection()->getDriver();
+
+ if ($this->typeCastEnabled && $typeMap->toArray()) {
+ $statement = new CallbackStatement($statement, $driver, new FieldTypeConverter($typeMap, $driver));
+ }
+
+ foreach ($this->_resultDecorators as $f) {
+ $statement = new CallbackStatement($statement, $driver, $f);
+ }
+
+ return $statement;
+ }
+
+ /**
+ * Helper function used to build conditions by composing QueryExpression objects.
+ *
+ * @param string $part Name of the query part to append the new part to
+ * @param string|array|\Cake\Database\ExpressionInterface|\Closure|null $append Expression or builder function to append.
+ * to append.
+ * @param string $conjunction type of conjunction to be used to operate part
+ * @param array $types associative array of type names used to bind values to query
+ * @return void
+ */
+ protected function _conjugate(string $part, $append, $conjunction, array $types): void
+ {
+ $expression = $this->_parts[$part] ?: $this->newExpr();
+ if (empty($append)) {
+ $this->_parts[$part] = $expression;
+
+ return;
+ }
+
+ if ($append instanceof Closure) {
+ $append = $append($this->newExpr(), $this);
+ }
+
+ if ($expression->getConjunction() === $conjunction) {
+ $expression->add($append, $types);
+ } else {
+ $expression = $this->newExpr()
+ ->setConjunction($conjunction)
+ ->add([$expression, $append], $types);
+ }
+
+ $this->_parts[$part] = $expression;
+ $this->_dirty();
+ }
+
+ /**
+ * Marks a query as dirty, removing any preprocessed information
+ * from in memory caching.
+ *
+ * @return void
+ */
+ protected function _dirty(): void
+ {
+ $this->_dirty = true;
+
+ if ($this->_iterator && $this->_valueBinder) {
+ $this->getValueBinder()->reset();
+ }
+ }
+
+ /**
+ * Handles clearing iterator and cloning all expressions and value binders.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ $this->_iterator = null;
+ if ($this->_valueBinder !== null) {
+ $this->_valueBinder = clone $this->_valueBinder;
+ }
+ if ($this->_selectTypeMap !== null) {
+ $this->_selectTypeMap = clone $this->_selectTypeMap;
+ }
+ foreach ($this->_parts as $name => $part) {
+ if (empty($part)) {
+ continue;
+ }
+ if (is_array($part)) {
+ foreach ($part as $i => $piece) {
+ if ($piece instanceof ExpressionInterface) {
+ /** @psalm-suppress PossiblyUndefinedMethod */
+ $this->_parts[$name][$i] = clone $piece;
+ }
+ }
+ }
+ if ($part instanceof ExpressionInterface) {
+ $this->_parts[$name] = clone $part;
+ }
+ }
+ }
+
+ /**
+ * Returns string representation of this query (complete SQL statement).
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ return $this->sql();
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ try {
+ set_error_handler(
+ /** @return no-return */
+ function ($errno, $errstr) {
+ throw new RuntimeException($errstr, $errno);
+ },
+ E_ALL
+ );
+ $sql = $this->sql();
+ $params = $this->getValueBinder()->bindings();
+ } catch (RuntimeException $e) {
+ $sql = 'SQL could not be generated for this query as it is incomplete.';
+ $params = [];
+ } finally {
+ restore_error_handler();
+ }
+
+ return [
+ '(help)' => 'This is a Query object, to get the results execute or iterate it.',
+ 'sql' => $sql,
+ 'params' => $params,
+ 'defaultTypes' => $this->getDefaultTypes(),
+ 'decorators' => count($this->_resultDecorators),
+ 'executed' => $this->_iterator ? true : false,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/QueryCompiler.php b/app/vendor/cakephp/cakephp/src/Database/QueryCompiler.php
new file mode 100644
index 000000000..3f8bf33a5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/QueryCompiler.php
@@ -0,0 +1,455 @@
+ 'DELETE',
+ 'where' => ' WHERE %s',
+ 'group' => ' GROUP BY %s ',
+ 'having' => ' HAVING %s ',
+ 'order' => ' %s',
+ 'limit' => ' LIMIT %s',
+ 'offset' => ' OFFSET %s',
+ 'epilog' => ' %s',
+ ];
+
+ /**
+ * The list of query clauses to traverse for generating a SELECT statement
+ *
+ * @var array
+ */
+ protected $_selectParts = [
+ 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
+ 'limit', 'offset', 'union', 'epilog',
+ ];
+
+ /**
+ * The list of query clauses to traverse for generating an UPDATE statement
+ *
+ * @var array
+ */
+ protected $_updateParts = ['with', 'update', 'set', 'where', 'epilog'];
+
+ /**
+ * The list of query clauses to traverse for generating a DELETE statement
+ *
+ * @var array
+ */
+ protected $_deleteParts = ['with', 'delete', 'modifier', 'from', 'where', 'epilog'];
+
+ /**
+ * The list of query clauses to traverse for generating an INSERT statement
+ *
+ * @var array
+ */
+ protected $_insertParts = ['with', 'insert', 'values', 'epilog'];
+
+ /**
+ * Indicate whether or not this query dialect supports ordered unions.
+ *
+ * Overridden in subclasses.
+ *
+ * @var bool
+ */
+ protected $_orderedUnion = true;
+
+ /**
+ * Indicate whether aliases in SELECT clause need to be always quoted.
+ *
+ * @var bool
+ */
+ protected $_quotedSelectAliases = false;
+
+ /**
+ * Returns the SQL representation of the provided query after generating
+ * the placeholders for the bound values using the provided generator
+ *
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholders
+ * @return string
+ */
+ public function compile(Query $query, ValueBinder $binder): string
+ {
+ $sql = '';
+ $type = $query->type();
+ $query->traverseParts(
+ $this->_sqlCompiler($sql, $query, $binder),
+ $this->{"_{$type}Parts"}
+ );
+
+ // Propagate bound parameters from sub-queries if the
+ // placeholders can be found in the SQL statement.
+ if ($query->getValueBinder() !== $binder) {
+ foreach ($query->getValueBinder()->bindings() as $binding) {
+ $placeholder = ':' . $binding['placeholder'];
+ if (preg_match('/' . $placeholder . '(?:\W|$)/', $sql) > 0) {
+ $binder->bind($placeholder, $binding['value'], $binding['type']);
+ }
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * Returns a callable object that can be used to compile a SQL string representation
+ * of this query.
+ *
+ * @param string $sql initial sql string to append to
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return \Closure
+ */
+ protected function _sqlCompiler(string &$sql, Query $query, ValueBinder $binder): Closure
+ {
+ return function ($part, $partName) use (&$sql, $query, $binder) {
+ if (
+ $part === null ||
+ (is_array($part) && empty($part)) ||
+ ($part instanceof Countable && count($part) === 0)
+ ) {
+ return;
+ }
+
+ if ($part instanceof ExpressionInterface) {
+ $part = [$part->sql($binder)];
+ }
+ if (isset($this->_templates[$partName])) {
+ $part = $this->_stringifyExpressions((array)$part, $binder);
+ $sql .= sprintf($this->_templates[$partName], implode(', ', $part));
+
+ return;
+ }
+
+ $sql .= $this->{'_build' . $partName . 'Part'}($part, $query, $binder);
+ };
+ }
+
+ /**
+ * Helper function used to build the string representation of a `WITH` clause,
+ * it constructs the CTE definitions list and generates the `RECURSIVE`
+ * keyword when required.
+ *
+ * @param array $parts List of CTEs to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $recursive = false;
+ $expressions = [];
+ foreach ($parts as $cte) {
+ $recursive = $recursive || $cte->isRecursive();
+ $expressions[] = $cte->sql($binder);
+ }
+
+ $recursive = $recursive ? 'RECURSIVE ' : '';
+
+ return sprintf('WITH %s%s ', $recursive, implode(', ', $expressions));
+ }
+
+ /**
+ * Helper function used to build the string representation of a SELECT clause,
+ * it constructs the field list taking care of aliasing and
+ * converting expression objects to string. This function also constructs the
+ * DISTINCT clause for the query.
+ *
+ * @param array $parts list of fields to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildSelectPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $select = 'SELECT%s %s%s';
+ if ($this->_orderedUnion && $query->clause('union')) {
+ $select = '(SELECT%s %s%s';
+ }
+ $distinct = $query->clause('distinct');
+ $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
+
+ $driver = $query->getConnection()->getDriver();
+ $quoteIdentifiers = $driver->isAutoQuotingEnabled() || $this->_quotedSelectAliases;
+ $normalized = [];
+ $parts = $this->_stringifyExpressions($parts, $binder);
+ foreach ($parts as $k => $p) {
+ if (!is_numeric($k)) {
+ $p = $p . ' AS ';
+ if ($quoteIdentifiers) {
+ $p .= $driver->quoteIdentifier($k);
+ } else {
+ $p .= $k;
+ }
+ }
+ $normalized[] = $p;
+ }
+
+ if ($distinct === true) {
+ $distinct = 'DISTINCT ';
+ }
+
+ if (is_array($distinct)) {
+ $distinct = $this->_stringifyExpressions($distinct, $binder);
+ $distinct = sprintf('DISTINCT ON (%s) ', implode(', ', $distinct));
+ }
+
+ return sprintf($select, $modifiers, $distinct, implode(', ', $normalized));
+ }
+
+ /**
+ * Helper function used to build the string representation of a FROM clause,
+ * it constructs the tables list taking care of aliasing and
+ * converting expression objects to string.
+ *
+ * @param array $parts list of tables to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildFromPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $select = ' FROM %s';
+ $normalized = [];
+ $parts = $this->_stringifyExpressions($parts, $binder);
+ foreach ($parts as $k => $p) {
+ if (!is_numeric($k)) {
+ $p = $p . ' ' . $k;
+ }
+ $normalized[] = $p;
+ }
+
+ return sprintf($select, implode(', ', $normalized));
+ }
+
+ /**
+ * Helper function used to build the string representation of multiple JOIN clauses,
+ * it constructs the joins list taking care of aliasing and converting
+ * expression objects to string in both the table to be joined and the conditions
+ * to be used.
+ *
+ * @param array $parts list of joins to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildJoinPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $joins = '';
+ foreach ($parts as $join) {
+ if ($join['table'] instanceof ExpressionInterface) {
+ $join['table'] = '(' . $join['table']->sql($binder) . ')';
+ }
+
+ $joins .= sprintf(' %s JOIN %s %s', $join['type'], $join['table'], $join['alias']);
+
+ $condition = '';
+ if (isset($join['conditions']) && $join['conditions'] instanceof ExpressionInterface) {
+ $condition = $join['conditions']->sql($binder);
+ }
+ if (strlen($condition)) {
+ $joins .= " ON {$condition}";
+ } else {
+ $joins .= ' ON 1 = 1';
+ }
+ }
+
+ return $joins;
+ }
+
+ /**
+ * Helper function to build the string representation of a window clause.
+ *
+ * @param array $parts List of windows to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildWindowPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $windows = [];
+ foreach ($parts as $window) {
+ $windows[] = $window['name']->sql($binder) . ' AS (' . $window['window']->sql($binder) . ')';
+ }
+
+ return ' WINDOW ' . implode(', ', $windows);
+ }
+
+ /**
+ * Helper function to generate SQL for SET expressions.
+ *
+ * @param array $parts List of keys & values to set.
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildSetPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $set = [];
+ foreach ($parts as $part) {
+ if ($part instanceof ExpressionInterface) {
+ $part = $part->sql($binder);
+ }
+ if ($part[0] === '(') {
+ $part = substr($part, 1, -1);
+ }
+ $set[] = $part;
+ }
+
+ return ' SET ' . implode('', $set);
+ }
+
+ /**
+ * Builds the SQL string for all the UNION clauses in this query, when dealing
+ * with query objects it will also transform them using their configured SQL
+ * dialect.
+ *
+ * @param array $parts list of queries to be operated with UNION
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildUnionPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $parts = array_map(function ($p) use ($binder) {
+ $p['query'] = $p['query']->sql($binder);
+ $p['query'] = $p['query'][0] === '(' ? trim($p['query'], '()') : $p['query'];
+ $prefix = $p['all'] ? 'ALL ' : '';
+ if ($this->_orderedUnion) {
+ return "{$prefix}({$p['query']})";
+ }
+
+ return $prefix . $p['query'];
+ }, $parts);
+
+ if ($this->_orderedUnion) {
+ return sprintf(")\nUNION %s", implode("\nUNION ", $parts));
+ }
+
+ return sprintf("\nUNION %s", implode("\nUNION ", $parts));
+ }
+
+ /**
+ * Builds the SQL fragment for INSERT INTO.
+ *
+ * @param array $parts The insert parts.
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string SQL fragment.
+ */
+ protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ if (!isset($parts[0])) {
+ throw new DatabaseException(
+ 'Could not compile insert query. No table was specified. ' .
+ 'Use `into()` to define a table.'
+ );
+ }
+ $table = $parts[0];
+ $columns = $this->_stringifyExpressions($parts[1], $binder);
+ $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
+
+ return sprintf('INSERT%s INTO %s (%s)', $modifiers, $table, implode(', ', $columns));
+ }
+
+ /**
+ * Builds the SQL fragment for INSERT INTO.
+ *
+ * @param array $parts The values parts.
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string SQL fragment.
+ */
+ protected function _buildValuesPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ return implode('', $this->_stringifyExpressions($parts, $binder));
+ }
+
+ /**
+ * Builds the SQL fragment for UPDATE.
+ *
+ * @param array $parts The update parts.
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string SQL fragment.
+ */
+ protected function _buildUpdatePart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $table = $this->_stringifyExpressions($parts, $binder);
+ $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
+
+ return sprintf('UPDATE%s %s', $modifiers, implode(',', $table));
+ }
+
+ /**
+ * Builds the SQL modifier fragment
+ *
+ * @param array $parts The query modifier parts
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string SQL fragment.
+ */
+ protected function _buildModifierPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ if ($parts === []) {
+ return '';
+ }
+
+ return ' ' . implode(' ', $this->_stringifyExpressions($parts, $binder, false));
+ }
+
+ /**
+ * Helper function used to covert ExpressionInterface objects inside an array
+ * into their string representation.
+ *
+ * @param array $expressions list of strings and ExpressionInterface objects
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @param bool $wrap Whether to wrap each expression object with parenthesis
+ * @return array
+ */
+ protected function _stringifyExpressions(array $expressions, ValueBinder $binder, bool $wrap = true): array
+ {
+ $result = [];
+ foreach ($expressions as $k => $expression) {
+ if ($expression instanceof ExpressionInterface) {
+ $value = $expression->sql($binder);
+ $expression = $wrap ? '(' . $value . ')' : $value;
+ }
+ $result[$k] = $expression;
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/README.md b/app/vendor/cakephp/cakephp/src/Database/README.md
new file mode 100644
index 000000000..877c7b68d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/README.md
@@ -0,0 +1,364 @@
+[](https://packagist.org/packages/cakephp/database)
+[](LICENSE.txt)
+
+# A flexible and lightweight Database Library for PHP
+
+This library abstracts and provides help with most aspects of dealing with relational
+databases such as keeping connections to the server, building queries,
+preventing SQL injections, inspecting and altering schemas, and with debugging and
+profiling queries sent to the database.
+
+It adopts the API from the native PDO extension in PHP for familiarity, but solves many of the
+inconsistencies PDO has, while also providing several features that extend PDO's capabilities.
+
+A distinguishing factor of this library when compared to similar database connection packages,
+is that it takes the concept of "data types" to its core. It lets you work with complex PHP objects
+or structures that can be passed as query conditions or to be inserted in the database.
+
+The typing system will intelligently convert the PHP structures when passing them to the database, and
+convert them back when retrieving.
+
+
+## Connecting to the database
+
+This library is able to work with the following databases:
+
+* MySQL
+* Postgres
+* SQLite
+* Microsoft SQL Server (2008 and above)
+
+The first thing you need to do when using this library is create a connection object.
+Before performing any operations with the connection, you need to specify a driver
+to use:
+
+```php
+use Cake\Database\Connection;
+use Cake\Database\Driver\Mysql;
+
+$driver = new Mysql([
+ 'database' => 'test',
+ 'username' => 'root',
+ 'password' => 'secret'
+]);
+$connection = new Connection([
+ 'driver' => $driver
+]);
+```
+
+Drivers are classes responsible for actually executing the commands to the database and
+correctly building the SQL according to the database specific dialect. Drivers can also
+be specified by passing a class name. In that case, include all the connection details
+directly in the options array:
+
+```php
+use Cake\Database\Connection;
+
+$connection = new Connection([
+ 'driver' => Cake\Database\Driver\Sqlite::class,
+ 'database' => '/path/to/file.db'
+]);
+```
+
+### Connection options
+
+This is a list of possible options that can be passed when creating a connection:
+
+* `persistent`: Creates a persistent connection
+* `host`: The server host
+* `database`: The database name
+* `username`: Login credential
+* `password`: Connection secret
+* `encoding`: The connection encoding (or charset)
+* `timezone`: The connection timezone or time offset
+
+## Using connections
+
+After creating a connection, you can immediately interact with the database. You can choose
+either to use the shorthand methods `execute()`, `insert()`, `update()`, `delete()` or use the
+`newQuery()` for using a query builder.
+
+The easiest way of executing queries is by using the `execute()` method, it will return a
+`Cake\Database\StatementInterface` that you can use to get the data back:
+
+```php
+$statement = $connection->execute('SELECT * FROM articles');
+
+while($row = $statement->fetch('assoc')) {
+ echo $row['title'] . PHP_EOL;
+}
+```
+Binding values to parametrized arguments is also possible with the execute function:
+
+```php
+$statement = $connection->execute('SELECT * FROM articles WHERE id = :id', ['id' => 1], ['id' => 'integer']);
+$results = $statement->fetch('assoc');
+```
+
+The third parameter is the types the passed values should be converted to when passed to the database. If
+no types are passed, all arguments will be interpreted as a string.
+
+Alternatively you can construct a statement manually and then fetch rows from it:
+
+```php
+$statement = $connection->prepare('SELECT * from articles WHERE id != :id');
+$statement->bind(['id' => 1], ['id' => 'integer']);
+$results = $statement->fetchAll('assoc');
+```
+
+The default types that are understood by this library and can be passed to the `bind()` function or to `execute()`
+are:
+
+* biginteger
+* binary
+* date
+* float
+* decimal
+* integer
+* time
+* datetime
+* timestamp
+* uuid
+
+More types can be added dynamically in a bit.
+
+Statements can be reused by binding new values to the parameters in the query:
+
+```php
+$statement = $connection->prepare('SELECT * from articles WHERE id = :id');
+$statement->bind(['id' => 1], ['id' => 'integer']);
+$results = $statement->fetchAll('assoc');
+
+$statement->bind(['id' => 1], ['id' => 'integer']);
+$results = $statement->fetchAll('assoc');
+```
+
+### Updating Rows
+
+Updating can be done using the `update()` function in the connection object. In the following
+example we will update the title of the article with id = 1:
+
+```php
+$connection->update('articles', ['title' => 'New title'], ['id' => 1]);
+```
+
+The concept of data types is central to this library, so you can use the last parameter of the function
+to specify what types should be used:
+
+```php
+$connection->update(
+ 'articles',
+ ['title' => 'New title'],
+ ['created >=' => new DateTime('-3 day'), 'created <' => new DateTime('now')],
+ ['created' => 'datetime']
+);
+```
+
+The example above will execute the following SQL:
+
+```sql
+UPDATE articles SET title = 'New Title' WHERE created >= '2014-10-10 00:00:00' AND created < '2014-10-13 00:00:00';
+```
+
+More on creating complex where conditions or more complex update queries later.
+
+### Deleting Rows
+
+Similarly, the `delete()` method is used to delete rows from the database:
+
+```php
+$connection->delete('articles', ['created <' => DateTime('now')], ['created' => 'date']);
+```
+
+Will generate the following SQL
+
+```sql
+DELETE FROM articles where created < '2014-10-10'
+```
+
+### Inserting Rows
+
+Rows can be inserted using the `insert()` method:
+
+```php
+$connection->insert(
+ 'articles',
+ ['title' => 'My Title', 'body' => 'Some paragraph', 'created' => new DateTime()],
+ ['created' => 'datetime']
+);
+```
+
+More complex updates, deletes and insert queries can be generated using the `Query` class.
+
+## Query Builder
+
+One of the goals of this library is to allow the generation of both simple and complex queries with
+ease. The query builder can be accessed by getting a new instance of a query:
+
+```php
+$query = $connection->newQuery();
+```
+
+### Selecting Fields
+
+Adding fields to the `SELECT` clause:
+
+```php
+$query->select(['id', 'title', 'body']);
+
+// Results in SELECT id AS pk, title AS aliased_title, body ...
+$query->select(['pk' => 'id', 'aliased_title' => 'title', 'body']);
+
+// Use a closure
+$query->select(function ($query) {
+ return ['id', 'title', 'body'];
+});
+```
+
+### Where Conditions
+
+Generating conditions:
+
+```php
+// WHERE id = 1
+$query->where(['id' => 1]);
+
+// WHERE id > 2
+$query->where(['id >' => 1]);
+```
+
+As you can see you can use any operator by placing it with a space after the field name.
+Adding multiple conditions is easy as well:
+
+```php
+$query->where(['id >' => 1])->andWhere(['title' => 'My Title']);
+
+// Equivalent to
+$query->where(['id >' => 1, 'title' => 'My title']);
+```
+
+It is possible to generate `OR` conditions as well
+
+```php
+$query->where(['OR' => ['id >' => 1, 'title' => 'My title']]);
+```
+
+For even more complex conditions you can use closures and expression objects:
+
+```php
+$query->where(function ($exp) {
+ return $exp
+ ->eq('author_id', 2)
+ ->eq('published', true)
+ ->notEq('spam', true)
+ ->gt('view_count', 10);
+ });
+```
+
+Which results in:
+
+```sql
+SELECT * FROM articles
+WHERE
+ author_id = 2
+ AND published = 1
+ AND spam != 1
+ AND view_count > 10
+```
+
+Combining expressions is also possible:
+
+```php
+$query->where(function ($exp) {
+ $orConditions = $exp->or(['author_id' => 2])
+ ->eq('author_id', 5);
+ return $exp
+ ->not($orConditions)
+ ->lte('view_count', 10);
+ });
+```
+
+That generates:
+
+```sql
+SELECT *
+FROM articles
+WHERE
+ NOT (author_id = 2 OR author_id = 5)
+ AND view_count <= 10
+```
+
+When using the expression objects you can use the following methods to create conditions:
+
+* `eq()` Creates an equality condition.
+* `notEq()` Create an inequality condition
+* `like()` Create a condition using the LIKE operator.
+* `notLike()` Create a negated LIKE condition.
+* `in()` Create a condition using IN.
+* `notIn()` Create a negated condition using IN.
+* `gt()` Create a > condition.
+* `gte()` Create a >= condition.
+* `lt()` Create a < condition.
+* `lte()` Create a <= condition.
+* `isNull()` Create an IS NULL condition.
+* `isNotNull()` Create a negated IS NULL condition.
+
+### Aggregates and SQL Functions
+
+```php
+// Results in SELECT COUNT(*) count FROM ...
+$query->select(['count' => $query->func()->count('*')]);
+```
+
+A number of commonly used functions can be created with the func() method:
+
+* `sum()` Calculate a sum. The arguments will be treated as literal values.
+* `avg()` Calculate an average. The arguments will be treated as literal values.
+* `min()` Calculate the min of a column. The arguments will be treated as literal values.
+* `max()` Calculate the max of a column. The arguments will be treated as literal values.
+* `count()` Calculate the count. The arguments will be treated as literal values.
+* `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal.
+* `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal.
+* `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal.
+* `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date.
+
+When providing arguments for SQL functions, there are two kinds of parameters you can use, literal arguments and bound parameters. Literal
+parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions.
+For example:
+
+```php
+$concat = $query->func()->concat([
+ 'title' => 'literal',
+ ' NEW'
+]);
+$query->select(['title' => $concat]);
+```
+
+The above generates:
+
+```sql
+SELECT CONCAT(title, :c0) ...;
+```
+
+### Other SQL Clauses
+
+Read of all other SQL clauses that the builder is capable of generating in the [official API docs](https://api.cakephp.org/4.x/class-Cake.Database.Query.html)
+
+### Getting Results out of a Query
+
+Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this:
+
+```php
+// Iterate the query
+foreach ($query as $row) {
+ // Do stuff.
+}
+
+// Get the statement and fetch all results
+$results = $query->execute()->fetchAll('assoc');
+```
+
+## Official API
+
+You can read the official [official API docs](https://api.cakephp.org/4.x/namespace-Cake.Database.html) to learn more of what this library
+has to offer.
diff --git a/app/vendor/cakephp/cakephp/src/Database/Retry/ErrorCodeWaitStrategy.php b/app/vendor/cakephp/cakephp/src/Database/Retry/ErrorCodeWaitStrategy.php
new file mode 100644
index 000000000..564dcc414
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Retry/ErrorCodeWaitStrategy.php
@@ -0,0 +1,69 @@
+errorCodes = $errorCodes;
+ $this->retryInterval = $retryInterval;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function shouldRetry(Exception $exception, int $retryCount): bool
+ {
+ if (
+ $exception instanceof PDOException &&
+ $exception->errorInfo &&
+ in_array($exception->errorInfo[1], $this->errorCodes)
+ ) {
+ if ($this->retryInterval > 0) {
+ sleep($this->retryInterval);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Retry/ReconnectStrategy.php b/app/vendor/cakephp/cakephp/src/Database/Retry/ReconnectStrategy.php
new file mode 100644
index 000000000..fc85a1ef6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Retry/ReconnectStrategy.php
@@ -0,0 +1,122 @@
+connection = $connection;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Checks whether or not the exception was caused by a lost connection,
+ * and returns true if it was able to successfully reconnect.
+ */
+ public function shouldRetry(Exception $exception, int $retryCount): bool
+ {
+ $message = $exception->getMessage();
+
+ foreach (static::$causes as $cause) {
+ if (strstr($message, $cause) !== false) {
+ return $this->reconnect();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Tries to re-establish the connection to the server, if it is safe to do so
+ *
+ * @return bool Whether or not the connection was re-established
+ */
+ protected function reconnect(): bool
+ {
+ if ($this->connection->inTransaction()) {
+ // It is not safe to blindly reconnect in the middle of a transaction
+ return false;
+ }
+
+ try {
+ // Make sure we free any resources associated with the old connection
+ $this->connection->disconnect();
+ } catch (Exception $e) {
+ }
+
+ try {
+ $this->connection->connect();
+ $this->connection->log('[RECONNECT]');
+
+ return true;
+ } catch (Exception $e) {
+ // If there was an error connecting again, don't report it back,
+ // let the retry handler do it.
+ return false;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/BaseSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/BaseSchema.php
new file mode 100644
index 000000000..46513170d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/BaseSchema.php
@@ -0,0 +1,5 @@
+collection = $collection;
+ $this->prefix = $prefix;
+ $this->cacher = $cacher;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function listTables(): array
+ {
+ return $this->collection->listTables();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describe(string $name, array $options = []): TableSchemaInterface
+ {
+ $options += ['forceRefresh' => false];
+ $cacheKey = $this->cacheKey($name);
+
+ if (!$options['forceRefresh']) {
+ $cached = $this->cacher->get($cacheKey);
+ if ($cached !== null) {
+ return $cached;
+ }
+ }
+
+ $table = $this->collection->describe($name, $options);
+ $this->cacher->set($cacheKey, $table);
+
+ return $table;
+ }
+
+ /**
+ * Get the cache key for a given name.
+ *
+ * @param string $name The name to get a cache key for.
+ * @return string The cache key.
+ */
+ public function cacheKey(string $name): string
+ {
+ return $this->prefix . '_' . $name;
+ }
+
+ /**
+ * Set a cacher.
+ *
+ * @param \Psr\SimpleCache\CacheInterface $cacher Cacher object
+ * @return $this
+ */
+ public function setCacher(CacheInterface $cacher)
+ {
+ $this->cacher = $cacher;
+
+ return $this;
+ }
+
+ /**
+ * Get a cacher.
+ *
+ * @return \Psr\SimpleCache\CacheInterface $cacher Cacher object
+ */
+ public function getCacher(): CacheInterface
+ {
+ return $this->cacher;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/Collection.php b/app/vendor/cakephp/cakephp/src/Database/Schema/Collection.php
new file mode 100644
index 000000000..0cf43cf66
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/Collection.php
@@ -0,0 +1,150 @@
+_connection = $connection;
+ $this->_dialect = $connection->getDriver()->schemaDialect();
+ }
+
+ /**
+ * Get the list of tables available in the current connection.
+ *
+ * @return string[] The list of tables in the connected database/schema.
+ */
+ public function listTables(): array
+ {
+ [$sql, $params] = $this->_dialect->listTablesSql($this->_connection->config());
+ $result = [];
+ $statement = $this->_connection->execute($sql, $params);
+ while ($row = $statement->fetch()) {
+ $result[] = $row[0];
+ }
+ $statement->closeCursor();
+
+ return $result;
+ }
+
+ /**
+ * Get the column metadata for a table.
+ *
+ * The name can include a database schema name in the form 'schema.table'.
+ *
+ * Caching will be applied if `cacheMetadata` key is present in the Connection
+ * configuration options. Defaults to _cake_model_ when true.
+ *
+ * ### Options
+ *
+ * - `forceRefresh` - Set to true to force rebuilding the cached metadata.
+ * Defaults to false.
+ *
+ * @param string $name The name of the table to describe.
+ * @param array $options The options to use, see above.
+ * @return \Cake\Database\Schema\TableSchema Object with column metadata.
+ * @throws \Cake\Database\Exception\DatabaseException when table cannot be described.
+ */
+ public function describe(string $name, array $options = []): TableSchemaInterface
+ {
+ $config = $this->_connection->config();
+ if (strpos($name, '.')) {
+ [$config['schema'], $name] = explode('.', $name);
+ }
+ $table = $this->_connection->getDriver()->newTableSchema($name);
+
+ $this->_reflect('Column', $name, $config, $table);
+ if (count($table->columns()) === 0) {
+ throw new DatabaseException(sprintf('Cannot describe %s. It has 0 columns.', $name));
+ }
+
+ $this->_reflect('Index', $name, $config, $table);
+ $this->_reflect('ForeignKey', $name, $config, $table);
+ $this->_reflect('Options', $name, $config, $table);
+
+ return $table;
+ }
+
+ /**
+ * Helper method for running each step of the reflection process.
+ *
+ * @param string $stage The stage name.
+ * @param string $name The table name.
+ * @param array $config The config data.
+ * @param \Cake\Database\Schema\TableSchema $schema The table schema instance.
+ * @return void
+ * @throws \Cake\Database\Exception\DatabaseException on query failure.
+ * @uses \Cake\Database\Schema\SchemaDialect::describeColumnSql
+ * @uses \Cake\Database\Schema\SchemaDialect::describeIndexSql
+ * @uses \Cake\Database\Schema\SchemaDialect::describeForeignKeySql
+ * @uses \Cake\Database\Schema\SchemaDialect::describeOptionsSql
+ * @uses \Cake\Database\Schema\SchemaDialect::convertColumnDescription
+ * @uses \Cake\Database\Schema\SchemaDialect::convertIndexDescription
+ * @uses \Cake\Database\Schema\SchemaDialect::convertForeignKeyDescription
+ * @uses \Cake\Database\Schema\SchemaDialect::convertOptionsDescription
+ */
+ protected function _reflect(string $stage, string $name, array $config, TableSchema $schema): void
+ {
+ $describeMethod = "describe{$stage}Sql";
+ $convertMethod = "convert{$stage}Description";
+
+ [$sql, $params] = $this->_dialect->{$describeMethod}($name, $config);
+ if (empty($sql)) {
+ return;
+ }
+ try {
+ $statement = $this->_connection->execute($sql, $params);
+ } catch (PDOException $e) {
+ throw new DatabaseException($e->getMessage(), 500, $e);
+ }
+ /** @psalm-suppress PossiblyFalseIterator */
+ foreach ($statement->fetchAll('assoc') as $row) {
+ $this->_dialect->{$convertMethod}($schema, $row);
+ }
+ $statement->closeCursor();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/CollectionInterface.php b/app/vendor/cakephp/cakephp/src/Database/Schema/CollectionInterface.php
new file mode 100644
index 000000000..2e3189765
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/CollectionInterface.php
@@ -0,0 +1,51 @@
+_driver->quoteIdentifier($config['database']), []];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeColumnSql(string $tableName, array $config): array
+ {
+ return ['SHOW FULL COLUMNS FROM ' . $this->_driver->quoteIdentifier($tableName), []];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeIndexSql(string $tableName, array $config): array
+ {
+ return ['SHOW INDEXES FROM ' . $this->_driver->quoteIdentifier($tableName), []];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeOptionsSql(string $tableName, array $config): array
+ {
+ return ['SHOW TABLE STATUS WHERE Name = ?', [$tableName]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertOptionsDescription(TableSchema $schema, array $row): void
+ {
+ $schema->setOptions([
+ 'engine' => $row['Engine'],
+ 'collation' => $row['Collation'],
+ ]);
+ }
+
+ /**
+ * Convert a MySQL column type into an abstract type.
+ *
+ * The returned type will be a type that Cake\Database\TypeFactory can handle.
+ *
+ * @param string $column The column type + length
+ * @return array Array of column information.
+ * @throws \Cake\Database\Exception\DatabaseException When column type cannot be parsed.
+ */
+ protected function _convertColumn(string $column): array
+ {
+ preg_match('/([a-z]+)(?:\(([0-9,]+)\))?\s*([a-z]+)?/i', $column, $matches);
+ if (empty($matches)) {
+ throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
+ }
+
+ $col = strtolower($matches[1]);
+ $length = $precision = null;
+ if (isset($matches[2]) && strlen($matches[2])) {
+ $length = $matches[2];
+ if (strpos($matches[2], ',') !== false) {
+ [$length, $precision] = explode(',', $length);
+ }
+ $length = (int)$length;
+ $precision = (int)$precision;
+ }
+
+ if (in_array($col, ['date', 'time'])) {
+ return ['type' => $col, 'length' => null];
+ }
+ if (in_array($col, ['datetime', 'timestamp'])) {
+ $typeName = $col;
+ if ($length > 0) {
+ $typeName = $col . 'fractional';
+ }
+
+ return ['type' => $typeName, 'length' => null, 'precision' => $length];
+ }
+
+ if (($col === 'tinyint' && $length === 1) || $col === 'boolean') {
+ return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
+ }
+
+ $unsigned = (isset($matches[3]) && strtolower($matches[3]) === 'unsigned');
+ if (strpos($col, 'bigint') !== false || $col === 'bigint') {
+ return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => null, 'unsigned' => $unsigned];
+ }
+ if ($col === 'tinyint') {
+ return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => null, 'unsigned' => $unsigned];
+ }
+ if ($col === 'smallint') {
+ return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => null, 'unsigned' => $unsigned];
+ }
+ if (in_array($col, ['int', 'integer', 'mediumint'])) {
+ return ['type' => TableSchema::TYPE_INTEGER, 'length' => null, 'unsigned' => $unsigned];
+ }
+ if ($col === 'char' && $length === 36) {
+ return ['type' => TableSchema::TYPE_UUID, 'length' => null];
+ }
+ if ($col === 'char') {
+ return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
+ }
+ if (strpos($col, 'char') !== false) {
+ return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
+ }
+ if (strpos($col, 'text') !== false) {
+ $lengthName = substr($col, 0, -4);
+ $length = TableSchema::$columnLengths[$lengthName] ?? null;
+
+ return ['type' => TableSchema::TYPE_TEXT, 'length' => $length];
+ }
+ if ($col === 'binary' && $length === 16) {
+ return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
+ }
+ if (strpos($col, 'blob') !== false || in_array($col, ['binary', 'varbinary'])) {
+ $lengthName = substr($col, 0, -4);
+ $length = TableSchema::$columnLengths[$lengthName] ?? $length;
+
+ return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
+ }
+ if (strpos($col, 'float') !== false || strpos($col, 'double') !== false) {
+ return [
+ 'type' => TableSchema::TYPE_FLOAT,
+ 'length' => $length,
+ 'precision' => $precision,
+ 'unsigned' => $unsigned,
+ ];
+ }
+ if (strpos($col, 'decimal') !== false) {
+ return [
+ 'type' => TableSchema::TYPE_DECIMAL,
+ 'length' => $length,
+ 'precision' => $precision,
+ 'unsigned' => $unsigned,
+ ];
+ }
+
+ if (strpos($col, 'json') !== false) {
+ return ['type' => TableSchema::TYPE_JSON, 'length' => null];
+ }
+
+ return ['type' => TableSchema::TYPE_STRING, 'length' => null];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertColumnDescription(TableSchema $schema, array $row): void
+ {
+ $field = $this->_convertColumn($row['Type']);
+ $field += [
+ 'null' => $row['Null'] === 'YES',
+ 'default' => $row['Default'],
+ 'collate' => $row['Collation'],
+ 'comment' => $row['Comment'],
+ ];
+ if (isset($row['Extra']) && $row['Extra'] === 'auto_increment') {
+ $field['autoIncrement'] = true;
+ }
+ $schema->addColumn($row['Field'], $field);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertIndexDescription(TableSchema $schema, array $row): void
+ {
+ $type = null;
+ $columns = $length = [];
+
+ $name = $row['Key_name'];
+ if ($name === 'PRIMARY') {
+ $name = $type = TableSchema::CONSTRAINT_PRIMARY;
+ }
+
+ $columns[] = $row['Column_name'];
+
+ if ($row['Index_type'] === 'FULLTEXT') {
+ $type = TableSchema::INDEX_FULLTEXT;
+ } elseif ((int)$row['Non_unique'] === 0 && $type !== 'primary') {
+ $type = TableSchema::CONSTRAINT_UNIQUE;
+ } elseif ($type !== 'primary') {
+ $type = TableSchema::INDEX_INDEX;
+ }
+
+ if (!empty($row['Sub_part'])) {
+ $length[$row['Column_name']] = $row['Sub_part'];
+ }
+ $isIndex = (
+ $type === TableSchema::INDEX_INDEX ||
+ $type === TableSchema::INDEX_FULLTEXT
+ );
+ if ($isIndex) {
+ $existing = $schema->getIndex($name);
+ } else {
+ $existing = $schema->getConstraint($name);
+ }
+
+ // MySQL multi column indexes come back as multiple rows.
+ if (!empty($existing)) {
+ $columns = array_merge($existing['columns'], $columns);
+ $length = array_merge($existing['length'], $length);
+ }
+ if ($isIndex) {
+ $schema->addIndex($name, [
+ 'type' => $type,
+ 'columns' => $columns,
+ 'length' => $length,
+ ]);
+ } else {
+ $schema->addConstraint($name, [
+ 'type' => $type,
+ 'columns' => $columns,
+ 'length' => $length,
+ ]);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeForeignKeySql(string $tableName, array $config): array
+ {
+ $sql = 'SELECT * FROM information_schema.key_column_usage AS kcu
+ INNER JOIN information_schema.referential_constraints AS rc
+ ON (
+ kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
+ AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
+ )
+ WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND rc.TABLE_NAME = ?
+ ORDER BY kcu.ORDINAL_POSITION ASC';
+
+ return [$sql, [$config['database'], $tableName, $tableName]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertForeignKeyDescription(TableSchema $schema, array $row): void
+ {
+ $data = [
+ 'type' => TableSchema::CONSTRAINT_FOREIGN,
+ 'columns' => [$row['COLUMN_NAME']],
+ 'references' => [$row['REFERENCED_TABLE_NAME'], $row['REFERENCED_COLUMN_NAME']],
+ 'update' => $this->_convertOnClause($row['UPDATE_RULE']),
+ 'delete' => $this->_convertOnClause($row['DELETE_RULE']),
+ ];
+ $name = $row['CONSTRAINT_NAME'];
+ $schema->addConstraint($name, $data);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function truncateTableSql(TableSchema $schema): array
+ {
+ return [sprintf('TRUNCATE TABLE `%s`', $schema->name())];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
+ {
+ $content = implode(",\n", array_merge($columns, $constraints, $indexes));
+ $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
+ $content = sprintf("CREATE%sTABLE `%s` (\n%s\n)", $temporary, $schema->name(), $content);
+ $options = $schema->getOptions();
+ if (isset($options['engine'])) {
+ $content .= sprintf(' ENGINE=%s', $options['engine']);
+ }
+ if (isset($options['charset'])) {
+ $content .= sprintf(' DEFAULT CHARSET=%s', $options['charset']);
+ }
+ if (isset($options['collate'])) {
+ $content .= sprintf(' COLLATE=%s', $options['collate']);
+ }
+
+ return [$content];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function columnSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getColumn($name);
+ $out = $this->_driver->quoteIdentifier($name);
+ $nativeJson = $this->_driver->supportsNativeJson();
+
+ $typeMap = [
+ TableSchema::TYPE_TINYINTEGER => ' TINYINT',
+ TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
+ TableSchema::TYPE_INTEGER => ' INTEGER',
+ TableSchema::TYPE_BIGINTEGER => ' BIGINT',
+ TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
+ TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
+ TableSchema::TYPE_FLOAT => ' FLOAT',
+ TableSchema::TYPE_DECIMAL => ' DECIMAL',
+ TableSchema::TYPE_DATE => ' DATE',
+ TableSchema::TYPE_TIME => ' TIME',
+ TableSchema::TYPE_DATETIME => ' DATETIME',
+ TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME',
+ TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP',
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMP',
+ TableSchema::TYPE_CHAR => ' CHAR',
+ TableSchema::TYPE_UUID => ' CHAR(36)',
+ TableSchema::TYPE_JSON => $nativeJson ? ' JSON' : ' LONGTEXT',
+ ];
+ $specialMap = [
+ 'string' => true,
+ 'text' => true,
+ 'char' => true,
+ 'binary' => true,
+ ];
+ if (isset($typeMap[$data['type']])) {
+ $out .= $typeMap[$data['type']];
+ }
+ if (isset($specialMap[$data['type']])) {
+ switch ($data['type']) {
+ case TableSchema::TYPE_STRING:
+ $out .= ' VARCHAR';
+ if (!isset($data['length'])) {
+ $data['length'] = 255;
+ }
+ break;
+ case TableSchema::TYPE_TEXT:
+ $isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
+ if (empty($data['length']) || !$isKnownLength) {
+ $out .= ' TEXT';
+ break;
+ }
+
+ /** @var string $length */
+ $length = array_search($data['length'], TableSchema::$columnLengths);
+ $out .= ' ' . strtoupper($length) . 'TEXT';
+
+ break;
+ case TableSchema::TYPE_BINARY:
+ $isKnownLength = in_array($data['length'], TableSchema::$columnLengths);
+ if ($isKnownLength) {
+ /** @var string $length */
+ $length = array_search($data['length'], TableSchema::$columnLengths);
+ $out .= ' ' . strtoupper($length) . 'BLOB';
+ break;
+ }
+
+ if (empty($data['length'])) {
+ $out .= ' BLOB';
+ break;
+ }
+
+ if ($data['length'] > 2) {
+ $out .= ' VARBINARY(' . $data['length'] . ')';
+ } else {
+ $out .= ' BINARY(' . $data['length'] . ')';
+ }
+ break;
+ }
+ }
+ $hasLength = [
+ TableSchema::TYPE_INTEGER,
+ TableSchema::TYPE_CHAR,
+ TableSchema::TYPE_SMALLINTEGER,
+ TableSchema::TYPE_TINYINTEGER,
+ TableSchema::TYPE_STRING,
+ ];
+ if (in_array($data['type'], $hasLength, true) && isset($data['length'])) {
+ $out .= '(' . $data['length'] . ')';
+ }
+
+ $lengthAndPrecisionTypes = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
+ if (in_array($data['type'], $lengthAndPrecisionTypes, true) && isset($data['length'])) {
+ if (isset($data['precision'])) {
+ $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
+ } else {
+ $out .= '(' . (int)$data['length'] . ')';
+ }
+ }
+
+ $precisionTypes = [TableSchema::TYPE_DATETIME_FRACTIONAL, TableSchema::TYPE_TIMESTAMP_FRACTIONAL];
+ if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) {
+ $out .= '(' . (int)$data['precision'] . ')';
+ }
+
+ $hasUnsigned = [
+ TableSchema::TYPE_TINYINTEGER,
+ TableSchema::TYPE_SMALLINTEGER,
+ TableSchema::TYPE_INTEGER,
+ TableSchema::TYPE_BIGINTEGER,
+ TableSchema::TYPE_FLOAT,
+ TableSchema::TYPE_DECIMAL,
+ ];
+ if (
+ in_array($data['type'], $hasUnsigned, true) &&
+ isset($data['unsigned']) &&
+ $data['unsigned'] === true
+ ) {
+ $out .= ' UNSIGNED';
+ }
+
+ $hasCollate = [
+ TableSchema::TYPE_TEXT,
+ TableSchema::TYPE_CHAR,
+ TableSchema::TYPE_STRING,
+ ];
+ if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
+ $out .= ' COLLATE ' . $data['collate'];
+ }
+
+ if (isset($data['null']) && $data['null'] === false) {
+ $out .= ' NOT NULL';
+ }
+ $addAutoIncrement = (
+ $schema->getPrimaryKey() === [$name] &&
+ !$schema->hasAutoincrement() &&
+ !isset($data['autoIncrement'])
+ );
+ if (
+ in_array($data['type'], [TableSchema::TYPE_INTEGER, TableSchema::TYPE_BIGINTEGER]) &&
+ (
+ $data['autoIncrement'] === true ||
+ $addAutoIncrement
+ )
+ ) {
+ $out .= ' AUTO_INCREMENT';
+ }
+
+ $timestampTypes = [
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE,
+ ];
+ if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) {
+ $out .= ' NULL';
+ unset($data['default']);
+ }
+
+ $dateTimeTypes = [
+ TableSchema::TYPE_DATETIME,
+ TableSchema::TYPE_DATETIME_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE,
+ ];
+ if (
+ isset($data['default']) &&
+ in_array($data['type'], $dateTimeTypes) &&
+ strpos(strtolower($data['default']), 'current_timestamp') !== false
+ ) {
+ $out .= ' DEFAULT CURRENT_TIMESTAMP';
+ if (isset($data['precision'])) {
+ $out .= '(' . $data['precision'] . ')';
+ }
+ unset($data['default']);
+ }
+ if (isset($data['default'])) {
+ $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
+ unset($data['default']);
+ }
+ if (isset($data['comment']) && $data['comment'] !== '') {
+ $out .= ' COMMENT ' . $this->_driver->schemaValue($data['comment']);
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function constraintSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getConstraint($name);
+ if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+
+ return sprintf('PRIMARY KEY (%s)', implode(', ', $columns));
+ }
+
+ $out = '';
+ if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
+ $out = 'UNIQUE KEY ';
+ }
+ if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $out = 'CONSTRAINT ';
+ }
+ $out .= $this->_driver->quoteIdentifier($name);
+
+ return $this->_keySql($out, $data);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addConstraintSql(TableSchema $schema): array
+ {
+ $sqlPattern = 'ALTER TABLE %s ADD %s;';
+ $sql = [];
+
+ foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
+ $constraint = $schema->getConstraint($name);
+ if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropConstraintSql(TableSchema $schema): array
+ {
+ $sqlPattern = 'ALTER TABLE %s DROP FOREIGN KEY %s;';
+ $sql = [];
+
+ foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
+ $constraint = $schema->getConstraint($name);
+ if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $constraintName = $this->_driver->quoteIdentifier($name);
+ $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function indexSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getIndex($name);
+ $out = '';
+ if ($data['type'] === TableSchema::INDEX_INDEX) {
+ $out = 'KEY ';
+ }
+ if ($data['type'] === TableSchema::INDEX_FULLTEXT) {
+ $out = 'FULLTEXT KEY ';
+ }
+ $out .= $this->_driver->quoteIdentifier($name);
+
+ return $this->_keySql($out, $data);
+ }
+
+ /**
+ * Helper method for generating key SQL snippets.
+ *
+ * @param string $prefix The key prefix
+ * @param array $data Key data.
+ * @return string
+ */
+ protected function _keySql(string $prefix, array $data): string
+ {
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+ foreach ($data['columns'] as $i => $column) {
+ if (isset($data['length'][$column])) {
+ $columns[$i] .= sprintf('(%d)', $data['length'][$column]);
+ }
+ }
+ if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ return $prefix . sprintf(
+ ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
+ implode(', ', $columns),
+ $this->_driver->quoteIdentifier($data['references'][0]),
+ $this->_convertConstraintColumns($data['references'][1]),
+ $this->_foreignOnClause($data['update']),
+ $this->_foreignOnClause($data['delete'])
+ );
+ }
+
+ return $prefix . ' (' . implode(', ', $columns) . ')';
+ }
+}
+
+// phpcs:disable
+// Add backwards compatible alias.
+class_alias('Cake\Database\Schema\MysqlSchemaDialect', 'Cake\Database\Schema\MysqlSchema');
+// phpcs:enable
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/PostgresSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/PostgresSchema.php
new file mode 100644
index 000000000..c74646505
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/PostgresSchema.php
@@ -0,0 +1,5 @@
+ $col, 'length' => null];
+ }
+ if (in_array($col, ['timestamptz', 'timestamp with time zone'], true)) {
+ return ['type' => TableSchema::TYPE_TIMESTAMP_TIMEZONE, 'length' => null];
+ }
+ if (strpos($col, 'timestamp') !== false) {
+ return ['type' => TableSchema::TYPE_TIMESTAMP_FRACTIONAL, 'length' => null];
+ }
+ if (strpos($col, 'time') !== false) {
+ return ['type' => TableSchema::TYPE_TIME, 'length' => null];
+ }
+ if ($col === 'serial' || $col === 'integer') {
+ return ['type' => TableSchema::TYPE_INTEGER, 'length' => 10];
+ }
+ if ($col === 'bigserial' || $col === 'bigint') {
+ return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => 20];
+ }
+ if ($col === 'smallint') {
+ return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => 5];
+ }
+ if ($col === 'inet') {
+ return ['type' => TableSchema::TYPE_STRING, 'length' => 39];
+ }
+ if ($col === 'uuid') {
+ return ['type' => TableSchema::TYPE_UUID, 'length' => null];
+ }
+ if ($col === 'char') {
+ return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
+ }
+ if (strpos($col, 'character') !== false) {
+ return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
+ }
+ // money is 'string' as it includes arbitrary text content
+ // before the number value.
+ if (strpos($col, 'money') !== false || $col === 'string') {
+ return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
+ }
+ if (strpos($col, 'text') !== false) {
+ return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
+ }
+ if ($col === 'bytea') {
+ return ['type' => TableSchema::TYPE_BINARY, 'length' => null];
+ }
+ if ($col === 'real' || strpos($col, 'double') !== false) {
+ return ['type' => TableSchema::TYPE_FLOAT, 'length' => null];
+ }
+ if (
+ strpos($col, 'numeric') !== false ||
+ strpos($col, 'decimal') !== false
+ ) {
+ return ['type' => TableSchema::TYPE_DECIMAL, 'length' => null];
+ }
+
+ if (strpos($col, 'json') !== false) {
+ return ['type' => TableSchema::TYPE_JSON, 'length' => null];
+ }
+
+ $length = is_numeric($length) ? $length : null;
+
+ return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertColumnDescription(TableSchema $schema, array $row): void
+ {
+ $field = $this->_convertColumn($row['type']);
+
+ if ($field['type'] === TableSchema::TYPE_BOOLEAN) {
+ if ($row['default'] === 'true') {
+ $row['default'] = 1;
+ }
+ if ($row['default'] === 'false') {
+ $row['default'] = 0;
+ }
+ }
+ if (!empty($row['has_serial'])) {
+ $field['autoIncrement'] = true;
+ }
+
+ $field += [
+ 'default' => $this->_defaultValue($row['default']),
+ 'null' => $row['null'] === 'YES',
+ 'collate' => $row['collation_name'],
+ 'comment' => $row['comment'],
+ ];
+ $field['length'] = $row['char_length'] ?: $field['length'];
+
+ if ($field['type'] === 'numeric' || $field['type'] === 'decimal') {
+ $field['length'] = $row['column_precision'];
+ $field['precision'] = $row['column_scale'] ?: null;
+ }
+
+ if ($field['type'] === TableSchema::TYPE_TIMESTAMP_FRACTIONAL) {
+ $field['precision'] = $row['datetime_precision'];
+ if ($field['precision'] === 0) {
+ $field['type'] = TableSchema::TYPE_TIMESTAMP;
+ }
+ }
+
+ if ($field['type'] === TableSchema::TYPE_TIMESTAMP_TIMEZONE) {
+ $field['precision'] = $row['datetime_precision'];
+ }
+
+ $schema->addColumn($row['name'], $field);
+ }
+
+ /**
+ * Manipulate the default value.
+ *
+ * Postgres includes sequence data and casting information in default values.
+ * We need to remove those.
+ *
+ * @param string|int|null $default The default value.
+ * @return string|int|null
+ */
+ protected function _defaultValue($default)
+ {
+ if (is_numeric($default) || $default === null) {
+ return $default;
+ }
+ // Sequences
+ if (strpos($default, 'nextval') === 0) {
+ return null;
+ }
+
+ if (strpos($default, 'NULL::') === 0) {
+ return null;
+ }
+
+ // Remove quotes and postgres casts
+ return preg_replace(
+ "/^'(.*)'(?:::.*)$/",
+ '$1',
+ $default
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeIndexSql(string $tableName, array $config): array
+ {
+ $sql = 'SELECT
+ c2.relname,
+ a.attname,
+ i.indisprimary,
+ i.indisunique
+ FROM pg_catalog.pg_namespace n
+ INNER JOIN pg_catalog.pg_class c ON (n.oid = c.relnamespace)
+ INNER JOIN pg_catalog.pg_index i ON (c.oid = i.indrelid)
+ INNER JOIN pg_catalog.pg_class c2 ON (c2.oid = i.indexrelid)
+ INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = c.oid AND i.indrelid::regclass = a.attrelid::regclass)
+ WHERE n.nspname = ?
+ AND a.attnum = ANY(i.indkey)
+ AND c.relname = ?
+ ORDER BY i.indisprimary DESC, i.indisunique DESC, c.relname, a.attnum';
+
+ $schema = 'public';
+ if (!empty($config['schema'])) {
+ $schema = $config['schema'];
+ }
+
+ return [$sql, [$schema, $tableName]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertIndexDescription(TableSchema $schema, array $row): void
+ {
+ $type = TableSchema::INDEX_INDEX;
+ $name = $row['relname'];
+ if ($row['indisprimary']) {
+ $name = $type = TableSchema::CONSTRAINT_PRIMARY;
+ }
+ if ($row['indisunique'] && $type === TableSchema::INDEX_INDEX) {
+ $type = TableSchema::CONSTRAINT_UNIQUE;
+ }
+ if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
+ $this->_convertConstraint($schema, $name, $type, $row);
+
+ return;
+ }
+ $index = $schema->getIndex($name);
+ if (!$index) {
+ $index = [
+ 'type' => $type,
+ 'columns' => [],
+ ];
+ }
+ $index['columns'][] = $row['attname'];
+ $schema->addIndex($name, $index);
+ }
+
+ /**
+ * Add/update a constraint into the schema object.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table to update.
+ * @param string $name The index name.
+ * @param string $type The index type.
+ * @param array $row The metadata record to update with.
+ * @return void
+ */
+ protected function _convertConstraint(TableSchema $schema, string $name, string $type, array $row): void
+ {
+ $constraint = $schema->getConstraint($name);
+ if (!$constraint) {
+ $constraint = [
+ 'type' => $type,
+ 'columns' => [],
+ ];
+ }
+ $constraint['columns'][] = $row['attname'];
+ $schema->addConstraint($name, $constraint);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeForeignKeySql(string $tableName, array $config): array
+ {
+ // phpcs:disable Generic.Files.LineLength
+ $sql = 'SELECT
+ c.conname AS name,
+ c.contype AS type,
+ a.attname AS column_name,
+ c.confmatchtype AS match_type,
+ c.confupdtype AS on_update,
+ c.confdeltype AS on_delete,
+ c.confrelid::regclass AS references_table,
+ ab.attname AS references_field
+ FROM pg_catalog.pg_namespace n
+ INNER JOIN pg_catalog.pg_class cl ON (n.oid = cl.relnamespace)
+ INNER JOIN pg_catalog.pg_constraint c ON (n.oid = c.connamespace)
+ INNER JOIN pg_catalog.pg_attribute a ON (a.attrelid = cl.oid AND c.conrelid = a.attrelid AND a.attnum = ANY(c.conkey))
+ INNER JOIN pg_catalog.pg_attribute ab ON (a.attrelid = cl.oid AND c.confrelid = ab.attrelid AND ab.attnum = ANY(c.confkey))
+ WHERE n.nspname = ?
+ AND cl.relname = ?
+ ORDER BY name, a.attnum, ab.attnum DESC';
+ // phpcs:enable Generic.Files.LineLength
+
+ $schema = empty($config['schema']) ? 'public' : $config['schema'];
+
+ return [$sql, [$schema, $tableName]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertForeignKeyDescription(TableSchema $schema, array $row): void
+ {
+ $data = [
+ 'type' => TableSchema::CONSTRAINT_FOREIGN,
+ 'columns' => $row['column_name'],
+ 'references' => [$row['references_table'], $row['references_field']],
+ 'update' => $this->_convertOnClause($row['on_update']),
+ 'delete' => $this->_convertOnClause($row['on_delete']),
+ ];
+ $schema->addConstraint($row['name'], $data);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _convertOnClause(string $clause): string
+ {
+ if ($clause === 'r') {
+ return TableSchema::ACTION_RESTRICT;
+ }
+ if ($clause === 'a') {
+ return TableSchema::ACTION_NO_ACTION;
+ }
+ if ($clause === 'c') {
+ return TableSchema::ACTION_CASCADE;
+ }
+
+ return TableSchema::ACTION_SET_NULL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function columnSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getColumn($name);
+ $out = $this->_driver->quoteIdentifier($name);
+ $typeMap = [
+ TableSchema::TYPE_TINYINTEGER => ' SMALLINT',
+ TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
+ TableSchema::TYPE_BINARY_UUID => ' UUID',
+ TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
+ TableSchema::TYPE_FLOAT => ' FLOAT',
+ TableSchema::TYPE_DECIMAL => ' DECIMAL',
+ TableSchema::TYPE_DATE => ' DATE',
+ TableSchema::TYPE_TIME => ' TIME',
+ TableSchema::TYPE_DATETIME => ' TIMESTAMP',
+ TableSchema::TYPE_DATETIME_FRACTIONAL => ' TIMESTAMP',
+ TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMP',
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTZ',
+ TableSchema::TYPE_UUID => ' UUID',
+ TableSchema::TYPE_CHAR => ' CHAR',
+ TableSchema::TYPE_JSON => ' JSONB',
+ ];
+
+ if (isset($typeMap[$data['type']])) {
+ $out .= $typeMap[$data['type']];
+ }
+
+ if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
+ $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' INTEGER' : ' BIGINT';
+ if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) {
+ $type = $data['type'] === TableSchema::TYPE_INTEGER ? ' SERIAL' : ' BIGSERIAL';
+ unset($data['null'], $data['default']);
+ }
+ $out .= $type;
+ }
+
+ if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
+ $out .= ' TEXT';
+ }
+ if ($data['type'] === TableSchema::TYPE_BINARY) {
+ $out .= ' BYTEA';
+ }
+
+ if ($data['type'] === TableSchema::TYPE_CHAR) {
+ $out .= '(' . $data['length'] . ')';
+ }
+
+ if (
+ $data['type'] === TableSchema::TYPE_STRING ||
+ (
+ $data['type'] === TableSchema::TYPE_TEXT &&
+ $data['length'] === TableSchema::LENGTH_TINY
+ )
+ ) {
+ $out .= ' VARCHAR';
+ if (isset($data['length']) && $data['length'] !== '') {
+ $out .= '(' . $data['length'] . ')';
+ }
+ }
+
+ $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR];
+ if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
+ $out .= ' COLLATE "' . $data['collate'] . '"';
+ }
+
+ $hasPrecision = [
+ TableSchema::TYPE_FLOAT,
+ TableSchema::TYPE_DATETIME,
+ TableSchema::TYPE_DATETIME_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE,
+ ];
+ if (in_array($data['type'], $hasPrecision) && isset($data['precision'])) {
+ $out .= '(' . $data['precision'] . ')';
+ }
+
+ if (
+ $data['type'] === TableSchema::TYPE_DECIMAL &&
+ (
+ isset($data['length']) ||
+ isset($data['precision'])
+ )
+ ) {
+ $out .= '(' . $data['length'] . ',' . (int)$data['precision'] . ')';
+ }
+
+ if (isset($data['null']) && $data['null'] === false) {
+ $out .= ' NOT NULL';
+ }
+
+ $datetimeTypes = [
+ TableSchema::TYPE_DATETIME,
+ TableSchema::TYPE_DATETIME_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE,
+ ];
+ if (
+ isset($data['default']) &&
+ in_array($data['type'], $datetimeTypes) &&
+ strtolower($data['default']) === 'current_timestamp'
+ ) {
+ $out .= ' DEFAULT CURRENT_TIMESTAMP';
+ } elseif (isset($data['default'])) {
+ $defaultValue = $data['default'];
+ if ($data['type'] === 'boolean') {
+ $defaultValue = (bool)$defaultValue;
+ }
+ $out .= ' DEFAULT ' . $this->_driver->schemaValue($defaultValue);
+ } elseif (isset($data['null']) && $data['null'] !== false) {
+ $out .= ' DEFAULT NULL';
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addConstraintSql(TableSchema $schema): array
+ {
+ $sqlPattern = 'ALTER TABLE %s ADD %s;';
+ $sql = [];
+
+ foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
+ $constraint = $schema->getConstraint($name);
+ if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropConstraintSql(TableSchema $schema): array
+ {
+ $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
+ $sql = [];
+
+ foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
+ $constraint = $schema->getConstraint($name);
+ if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $constraintName = $this->_driver->quoteIdentifier($name);
+ $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function indexSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getIndex($name);
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+
+ return sprintf(
+ 'CREATE INDEX %s ON %s (%s)',
+ $this->_driver->quoteIdentifier($name),
+ $this->_driver->quoteIdentifier($schema->name()),
+ implode(', ', $columns)
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function constraintSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getConstraint($name);
+ $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
+ if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
+ $out = 'PRIMARY KEY';
+ }
+ if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
+ $out .= ' UNIQUE';
+ }
+
+ return $this->_keySql($out, $data);
+ }
+
+ /**
+ * Helper method for generating key SQL snippets.
+ *
+ * @param string $prefix The key prefix
+ * @param array $data Key data.
+ * @return string
+ */
+ protected function _keySql(string $prefix, array $data): string
+ {
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+ if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ return $prefix . sprintf(
+ ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s DEFERRABLE INITIALLY IMMEDIATE',
+ implode(', ', $columns),
+ $this->_driver->quoteIdentifier($data['references'][0]),
+ $this->_convertConstraintColumns($data['references'][1]),
+ $this->_foreignOnClause($data['update']),
+ $this->_foreignOnClause($data['delete'])
+ );
+ }
+
+ return $prefix . ' (' . implode(', ', $columns) . ')';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
+ {
+ $content = array_merge($columns, $constraints);
+ $content = implode(",\n", array_filter($content));
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
+ $out = [];
+ $out[] = sprintf("CREATE%sTABLE %s (\n%s\n)", $temporary, $tableName, $content);
+ foreach ($indexes as $index) {
+ $out[] = $index;
+ }
+ foreach ($schema->columns() as $column) {
+ $columnData = $schema->getColumn($column);
+ if (isset($columnData['comment'])) {
+ $out[] = sprintf(
+ 'COMMENT ON COLUMN %s.%s IS %s',
+ $tableName,
+ $this->_driver->quoteIdentifier($column),
+ $this->_driver->schemaValue($columnData['comment'])
+ );
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function truncateTableSql(TableSchema $schema): array
+ {
+ $name = $this->_driver->quoteIdentifier($schema->name());
+
+ return [
+ sprintf('TRUNCATE %s RESTART IDENTITY CASCADE', $name),
+ ];
+ }
+
+ /**
+ * Generate the SQL to drop a table.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema Table instance
+ * @return array SQL statements to drop a table.
+ */
+ public function dropTableSql(TableSchema $schema): array
+ {
+ $sql = sprintf(
+ 'DROP TABLE %s CASCADE',
+ $this->_driver->quoteIdentifier($schema->name())
+ );
+
+ return [$sql];
+ }
+}
+
+// phpcs:disable
+// Add backwards compatible alias.
+class_alias('Cake\Database\Schema\PostgresSchemaDialect', 'Cake\Database\Schema\PostgresSchema');
+// phpcs:enable
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/SchemaDialect.php b/app/vendor/cakephp/cakephp/src/Database/Schema/SchemaDialect.php
new file mode 100644
index 000000000..948134703
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/SchemaDialect.php
@@ -0,0 +1,290 @@
+connect();
+ $this->_driver = $driver;
+ }
+
+ /**
+ * Generate an ON clause for a foreign key.
+ *
+ * @param string $on The on clause
+ * @return string
+ */
+ protected function _foreignOnClause(string $on): string
+ {
+ if ($on === TableSchema::ACTION_SET_NULL) {
+ return 'SET NULL';
+ }
+ if ($on === TableSchema::ACTION_SET_DEFAULT) {
+ return 'SET DEFAULT';
+ }
+ if ($on === TableSchema::ACTION_CASCADE) {
+ return 'CASCADE';
+ }
+ if ($on === TableSchema::ACTION_RESTRICT) {
+ return 'RESTRICT';
+ }
+ if ($on === TableSchema::ACTION_NO_ACTION) {
+ return 'NO ACTION';
+ }
+
+ throw new InvalidArgumentException('Invalid value for "on": ' . $on);
+ }
+
+ /**
+ * Convert string on clauses to the abstract ones.
+ *
+ * @param string $clause The on clause to convert.
+ * @return string
+ */
+ protected function _convertOnClause(string $clause): string
+ {
+ if ($clause === 'CASCADE' || $clause === 'RESTRICT') {
+ return strtolower($clause);
+ }
+ if ($clause === 'NO ACTION') {
+ return TableSchema::ACTION_NO_ACTION;
+ }
+
+ return TableSchema::ACTION_SET_NULL;
+ }
+
+ /**
+ * Convert foreign key constraints references to a valid
+ * stringified list
+ *
+ * @param string|array $references The referenced columns of a foreign key constraint statement
+ * @return string
+ */
+ protected function _convertConstraintColumns($references): string
+ {
+ if (is_string($references)) {
+ return $this->_driver->quoteIdentifier($references);
+ }
+
+ return implode(', ', array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $references
+ ));
+ }
+
+ /**
+ * Generate the SQL to drop a table.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema Schema instance
+ * @return array SQL statements to drop a table.
+ */
+ public function dropTableSql(TableSchema $schema): array
+ {
+ $sql = sprintf(
+ 'DROP TABLE %s',
+ $this->_driver->quoteIdentifier($schema->name())
+ );
+
+ return [$sql];
+ }
+
+ /**
+ * Generate the SQL to list the tables.
+ *
+ * @param array $config The connection configuration to use for
+ * getting tables from.
+ * @return array An array of (sql, params) to execute.
+ */
+ abstract public function listTablesSql(array $config): array;
+
+ /**
+ * Generate the SQL to describe a table.
+ *
+ * @param string $tableName The table name to get information on.
+ * @param array $config The connection configuration.
+ * @return array An array of (sql, params) to execute.
+ */
+ abstract public function describeColumnSql(string $tableName, array $config): array;
+
+ /**
+ * Generate the SQL to describe the indexes in a table.
+ *
+ * @param string $tableName The table name to get information on.
+ * @param array $config The connection configuration.
+ * @return array An array of (sql, params) to execute.
+ */
+ abstract public function describeIndexSql(string $tableName, array $config): array;
+
+ /**
+ * Generate the SQL to describe the foreign keys in a table.
+ *
+ * @param string $tableName The table name to get information on.
+ * @param array $config The connection configuration.
+ * @return array An array of (sql, params) to execute.
+ */
+ abstract public function describeForeignKeySql(string $tableName, array $config): array;
+
+ /**
+ * Generate the SQL to describe table options
+ *
+ * @param string $tableName Table name.
+ * @param array $config The connection configuration.
+ * @return array SQL statements to get options for a table.
+ */
+ public function describeOptionsSql(string $tableName, array $config): array
+ {
+ return ['', ''];
+ }
+
+ /**
+ * Convert field description results into abstract schema fields.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table object to append fields to.
+ * @param array $row The row data from `describeColumnSql`.
+ * @return void
+ */
+ abstract public function convertColumnDescription(TableSchema $schema, array $row): void;
+
+ /**
+ * Convert an index description results into abstract schema indexes or constraints.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table object to append
+ * an index or constraint to.
+ * @param array $row The row data from `describeIndexSql`.
+ * @return void
+ */
+ abstract public function convertIndexDescription(TableSchema $schema, array $row): void;
+
+ /**
+ * Convert a foreign key description into constraints on the Table object.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table object to append
+ * a constraint to.
+ * @param array $row The row data from `describeForeignKeySql`.
+ * @return void
+ */
+ abstract public function convertForeignKeyDescription(TableSchema $schema, array $row): void;
+
+ /**
+ * Convert options data into table options.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema Table instance.
+ * @param array $row The row of data.
+ * @return void
+ */
+ public function convertOptionsDescription(TableSchema $schema, array $row): void
+ {
+ }
+
+ /**
+ * Generate the SQL to create a table.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema Table instance.
+ * @param string[] $columns The columns to go inside the table.
+ * @param string[] $constraints The constraints for the table.
+ * @param string[] $indexes The indexes for the table.
+ * @return string[] SQL statements to create a table.
+ */
+ abstract public function createTableSql(
+ TableSchema $schema,
+ array $columns,
+ array $constraints,
+ array $indexes
+ ): array;
+
+ /**
+ * Generate the SQL fragment for a single column in a table.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
+ * @param string $name The name of the column.
+ * @return string SQL fragment.
+ */
+ abstract public function columnSql(TableSchema $schema, string $name): string;
+
+ /**
+ * Generate the SQL queries needed to add foreign key constraints to the table
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
+ * @return array SQL fragment.
+ */
+ abstract public function addConstraintSql(TableSchema $schema): array;
+
+ /**
+ * Generate the SQL queries needed to drop foreign key constraints from the table
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
+ * @return array SQL fragment.
+ */
+ abstract public function dropConstraintSql(TableSchema $schema): array;
+
+ /**
+ * Generate the SQL fragments for defining table constraints.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
+ * @param string $name The name of the column.
+ * @return string SQL fragment.
+ */
+ abstract public function constraintSql(TableSchema $schema, string $name): string;
+
+ /**
+ * Generate the SQL fragment for a single index in a table.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table object the column is in.
+ * @param string $name The name of the column.
+ * @return string SQL fragment.
+ */
+ abstract public function indexSql(TableSchema $schema, string $name): string;
+
+ /**
+ * Generate the SQL to truncate a table.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema Table instance.
+ * @return array SQL statements to truncate a table.
+ */
+ abstract public function truncateTableSql(TableSchema $schema): array;
+}
+
+// phpcs:disable
+// Add backwards compatible alias.
+class_alias('Cake\Database\Schema\SchemaDialect', 'Cake\Database\Schema\BaseSchema');
+// phpcs:enable
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/SqlGeneratorInterface.php b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlGeneratorInterface.php
new file mode 100644
index 000000000..3acaf27b9
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlGeneratorInterface.php
@@ -0,0 +1,72 @@
+ TableSchema::TYPE_TEXT, 'length' => null];
+ }
+
+ preg_match('/(unsigned)?\s*([a-z]+)(?:\(([0-9,]+)\))?/i', $column, $matches);
+ if (empty($matches)) {
+ throw new DatabaseException(sprintf('Unable to parse column type from "%s"', $column));
+ }
+
+ $unsigned = false;
+ if (strtolower($matches[1]) === 'unsigned') {
+ $unsigned = true;
+ }
+
+ $col = strtolower($matches[2]);
+ $length = $precision = null;
+ if (isset($matches[3])) {
+ $length = $matches[3];
+ if (strpos($length, ',') !== false) {
+ [$length, $precision] = explode(',', $length);
+ }
+ $length = (int)$length;
+ $precision = (int)$precision;
+ }
+
+ if ($col === 'bigint') {
+ return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $length, 'unsigned' => $unsigned];
+ }
+ if ($col === 'smallint') {
+ return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $length, 'unsigned' => $unsigned];
+ }
+ if ($col === 'tinyint') {
+ return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $length, 'unsigned' => $unsigned];
+ }
+ if (strpos($col, 'int') !== false) {
+ return ['type' => TableSchema::TYPE_INTEGER, 'length' => $length, 'unsigned' => $unsigned];
+ }
+ if (strpos($col, 'decimal') !== false) {
+ return [
+ 'type' => TableSchema::TYPE_DECIMAL,
+ 'length' => $length,
+ 'precision' => $precision,
+ 'unsigned' => $unsigned,
+ ];
+ }
+ if (in_array($col, ['float', 'real', 'double'])) {
+ return [
+ 'type' => TableSchema::TYPE_FLOAT,
+ 'length' => $length,
+ 'precision' => $precision,
+ 'unsigned' => $unsigned,
+ ];
+ }
+
+ if (strpos($col, 'boolean') !== false) {
+ return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
+ }
+
+ if ($col === 'char' && $length === 36) {
+ return ['type' => TableSchema::TYPE_UUID, 'length' => null];
+ }
+ if ($col === 'char') {
+ return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
+ }
+ if (strpos($col, 'char') !== false) {
+ return ['type' => TableSchema::TYPE_STRING, 'length' => $length];
+ }
+
+ if ($col === 'binary' && $length === 16) {
+ return ['type' => TableSchema::TYPE_BINARY_UUID, 'length' => null];
+ }
+ if (in_array($col, ['blob', 'clob', 'binary', 'varbinary'])) {
+ return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
+ }
+
+ $datetimeTypes = [
+ 'date',
+ 'time',
+ 'timestamp',
+ 'timestampfractional',
+ 'timestamptimezone',
+ 'datetime',
+ 'datetimefractional',
+ ];
+ if (in_array($col, $datetimeTypes)) {
+ return ['type' => $col, 'length' => null];
+ }
+
+ return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function listTablesSql(array $config): array
+ {
+ return [
+ 'SELECT name FROM sqlite_master WHERE type="table" ' .
+ 'AND name != "sqlite_sequence" ORDER BY name',
+ [],
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeColumnSql(string $tableName, array $config): array
+ {
+ $sql = sprintf(
+ 'PRAGMA table_info(%s)',
+ $this->_driver->quoteIdentifier($tableName)
+ );
+
+ return [$sql, []];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertColumnDescription(TableSchema $schema, array $row): void
+ {
+ $field = $this->_convertColumn($row['type']);
+ $field += [
+ 'null' => !$row['notnull'],
+ 'default' => $this->_defaultValue($row['dflt_value']),
+ ];
+ $primary = $schema->getConstraint('primary');
+
+ if ($row['pk'] && empty($primary)) {
+ $field['null'] = false;
+ $field['autoIncrement'] = true;
+ }
+
+ // SQLite does not support autoincrement on composite keys.
+ if ($row['pk'] && !empty($primary)) {
+ $existingColumn = $primary['columns'][0];
+ /** @psalm-suppress PossiblyNullOperand */
+ $schema->addColumn($existingColumn, ['autoIncrement' => null] + $schema->getColumn($existingColumn));
+ }
+
+ $schema->addColumn($row['name'], $field);
+ if ($row['pk']) {
+ $constraint = (array)$schema->getConstraint('primary') + [
+ 'type' => TableSchema::CONSTRAINT_PRIMARY,
+ 'columns' => [],
+ ];
+ $constraint['columns'] = array_merge($constraint['columns'], [$row['name']]);
+ $schema->addConstraint('primary', $constraint);
+ }
+ }
+
+ /**
+ * Manipulate the default value.
+ *
+ * Sqlite includes quotes and bared NULLs in default values.
+ * We need to remove those.
+ *
+ * @param string|int|null $default The default value.
+ * @return string|int|null
+ */
+ protected function _defaultValue($default)
+ {
+ if ($default === 'NULL' || $default === null) {
+ return null;
+ }
+
+ // Remove quotes
+ if (is_string($default) && preg_match("/^'(.*)'$/", $default, $matches)) {
+ return str_replace("''", "'", $matches[1]);
+ }
+
+ return $default;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeIndexSql(string $tableName, array $config): array
+ {
+ $sql = sprintf(
+ 'PRAGMA index_list(%s)',
+ $this->_driver->quoteIdentifier($tableName)
+ );
+
+ return [$sql, []];
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Since SQLite does not have a way to get metadata about all indexes at once,
+ * additional queries are done here. Sqlite constraint names are not
+ * stable, and the names for constraints will not match those used to create
+ * the table. This is a limitation in Sqlite's metadata features.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table object to append
+ * an index or constraint to.
+ * @param array $row The row data from `describeIndexSql`.
+ * @return void
+ */
+ public function convertIndexDescription(TableSchema $schema, array $row): void
+ {
+ $sql = sprintf(
+ 'PRAGMA index_info(%s)',
+ $this->_driver->quoteIdentifier($row['name'])
+ );
+ $statement = $this->_driver->prepare($sql);
+ $statement->execute();
+ $columns = [];
+ /** @psalm-suppress PossiblyFalseIterator */
+ foreach ($statement->fetchAll('assoc') as $column) {
+ $columns[] = $column['name'];
+ }
+ $statement->closeCursor();
+ if ($row['unique']) {
+ $schema->addConstraint($row['name'], [
+ 'type' => TableSchema::CONSTRAINT_UNIQUE,
+ 'columns' => $columns,
+ ]);
+ } else {
+ $schema->addIndex($row['name'], [
+ 'type' => TableSchema::INDEX_INDEX,
+ 'columns' => $columns,
+ ]);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeForeignKeySql(string $tableName, array $config): array
+ {
+ $sql = sprintf('PRAGMA foreign_key_list(%s)', $this->_driver->quoteIdentifier($tableName));
+
+ return [$sql, []];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertForeignKeyDescription(TableSchema $schema, array $row): void
+ {
+ $name = $row['from'] . '_fk';
+
+ $update = $row['on_update'] ?? '';
+ $delete = $row['on_delete'] ?? '';
+ $data = [
+ 'type' => TableSchema::CONSTRAINT_FOREIGN,
+ 'columns' => [$row['from']],
+ 'references' => [$row['table'], $row['to']],
+ 'update' => $this->_convertOnClause($update),
+ 'delete' => $this->_convertOnClause($delete),
+ ];
+
+ if (isset($this->_constraintsIdMap[$schema->name()][$row['id']])) {
+ $name = $this->_constraintsIdMap[$schema->name()][$row['id']];
+ } else {
+ $this->_constraintsIdMap[$schema->name()][$row['id']] = $name;
+ }
+
+ $schema->addConstraint($name, $data);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
+ * @param string $name The name of the column.
+ * @return string SQL fragment.
+ * @throws \Cake\Database\Exception\DatabaseException when the column type is unknown
+ */
+ public function columnSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getColumn($name);
+ $typeMap = [
+ TableSchema::TYPE_BINARY_UUID => ' BINARY(16)',
+ TableSchema::TYPE_UUID => ' CHAR(36)',
+ TableSchema::TYPE_CHAR => ' CHAR',
+ TableSchema::TYPE_TINYINTEGER => ' TINYINT',
+ TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
+ TableSchema::TYPE_INTEGER => ' INTEGER',
+ TableSchema::TYPE_BIGINTEGER => ' BIGINT',
+ TableSchema::TYPE_BOOLEAN => ' BOOLEAN',
+ TableSchema::TYPE_FLOAT => ' FLOAT',
+ TableSchema::TYPE_DECIMAL => ' DECIMAL',
+ TableSchema::TYPE_DATE => ' DATE',
+ TableSchema::TYPE_TIME => ' TIME',
+ TableSchema::TYPE_DATETIME => ' DATETIME',
+ TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIMEFRACTIONAL',
+ TableSchema::TYPE_TIMESTAMP => ' TIMESTAMP',
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' TIMESTAMPFRACTIONAL',
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' TIMESTAMPTIMEZONE',
+ TableSchema::TYPE_JSON => ' TEXT',
+ ];
+
+ $out = $this->_driver->quoteIdentifier($name);
+ $hasUnsigned = [
+ TableSchema::TYPE_TINYINTEGER,
+ TableSchema::TYPE_SMALLINTEGER,
+ TableSchema::TYPE_INTEGER,
+ TableSchema::TYPE_BIGINTEGER,
+ TableSchema::TYPE_FLOAT,
+ TableSchema::TYPE_DECIMAL,
+ ];
+
+ if (
+ in_array($data['type'], $hasUnsigned, true) &&
+ isset($data['unsigned']) &&
+ $data['unsigned'] === true
+ ) {
+ if ($data['type'] !== TableSchema::TYPE_INTEGER || $schema->getPrimaryKey() !== [$name]) {
+ $out .= ' UNSIGNED';
+ }
+ }
+
+ if (isset($typeMap[$data['type']])) {
+ $out .= $typeMap[$data['type']];
+ }
+
+ if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
+ $out .= ' TEXT';
+ }
+
+ if ($data['type'] === TableSchema::TYPE_CHAR) {
+ $out .= '(' . $data['length'] . ')';
+ }
+
+ if (
+ $data['type'] === TableSchema::TYPE_STRING ||
+ (
+ $data['type'] === TableSchema::TYPE_TEXT &&
+ $data['length'] === TableSchema::LENGTH_TINY
+ )
+ ) {
+ $out .= ' VARCHAR';
+
+ if (isset($data['length'])) {
+ $out .= '(' . $data['length'] . ')';
+ }
+ }
+
+ if ($data['type'] === TableSchema::TYPE_BINARY) {
+ if (isset($data['length'])) {
+ $out .= ' BLOB(' . $data['length'] . ')';
+ } else {
+ $out .= ' BLOB';
+ }
+ }
+
+ $integerTypes = [
+ TableSchema::TYPE_TINYINTEGER,
+ TableSchema::TYPE_SMALLINTEGER,
+ TableSchema::TYPE_INTEGER,
+ ];
+ if (
+ in_array($data['type'], $integerTypes, true) &&
+ isset($data['length']) &&
+ $schema->getPrimaryKey() !== [$name]
+ ) {
+ $out .= '(' . (int)$data['length'] . ')';
+ }
+
+ $hasPrecision = [TableSchema::TYPE_FLOAT, TableSchema::TYPE_DECIMAL];
+ if (
+ in_array($data['type'], $hasPrecision, true) &&
+ (
+ isset($data['length']) ||
+ isset($data['precision'])
+ )
+ ) {
+ $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
+ }
+
+ if (isset($data['null']) && $data['null'] === false) {
+ $out .= ' NOT NULL';
+ }
+
+ if ($data['type'] === TableSchema::TYPE_INTEGER && $schema->getPrimaryKey() === [$name]) {
+ $out .= ' PRIMARY KEY AUTOINCREMENT';
+ }
+
+ $timestampTypes = [
+ TableSchema::TYPE_DATETIME,
+ TableSchema::TYPE_DATETIME_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE,
+ ];
+ if (isset($data['null']) && $data['null'] === true && in_array($data['type'], $timestampTypes, true)) {
+ $out .= ' DEFAULT NULL';
+ }
+ if (isset($data['default'])) {
+ $out .= ' DEFAULT ' . $this->_driver->schemaValue($data['default']);
+ }
+
+ return $out;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Note integer primary keys will return ''. This is intentional as Sqlite requires
+ * that integer primary keys be defined in the column definition.
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the column is in.
+ * @param string $name The name of the column.
+ * @return string SQL fragment.
+ */
+ public function constraintSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getConstraint($name);
+ /** @psalm-suppress PossiblyNullArrayAccess */
+ if (
+ $data['type'] === TableSchema::CONSTRAINT_PRIMARY &&
+ count($data['columns']) === 1 &&
+ $schema->getColumn($data['columns'][0])['type'] === TableSchema::TYPE_INTEGER
+ ) {
+ return '';
+ }
+ $clause = '';
+ $type = '';
+ if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
+ $type = 'PRIMARY KEY';
+ }
+ if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
+ $type = 'UNIQUE';
+ }
+ if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $type = 'FOREIGN KEY';
+
+ $clause = sprintf(
+ ' REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
+ $this->_driver->quoteIdentifier($data['references'][0]),
+ $this->_convertConstraintColumns($data['references'][1]),
+ $this->_foreignOnClause($data['update']),
+ $this->_foreignOnClause($data['delete'])
+ );
+ }
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+
+ return sprintf(
+ 'CONSTRAINT %s %s (%s)%s',
+ $this->_driver->quoteIdentifier($name),
+ $type,
+ implode(', ', $columns),
+ $clause
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * SQLite can not properly handle adding a constraint to an existing table.
+ * This method is no-op
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
+ * @return array SQL fragment.
+ */
+ public function addConstraintSql(TableSchema $schema): array
+ {
+ return [];
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * SQLite can not properly handle dropping a constraint to an existing table.
+ * This method is no-op
+ *
+ * @param \Cake\Database\Schema\TableSchema $schema The table instance the foreign key constraints are.
+ * @return array SQL fragment.
+ */
+ public function dropConstraintSql(TableSchema $schema): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function indexSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getIndex($name);
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+
+ return sprintf(
+ 'CREATE INDEX %s ON %s (%s)',
+ $this->_driver->quoteIdentifier($name),
+ $this->_driver->quoteIdentifier($schema->name()),
+ implode(', ', $columns)
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
+ {
+ $lines = array_merge($columns, $constraints);
+ $content = implode(",\n", array_filter($lines));
+ $temporary = $schema->isTemporary() ? ' TEMPORARY ' : ' ';
+ $table = sprintf("CREATE%sTABLE \"%s\" (\n%s\n)", $temporary, $schema->name(), $content);
+ $out = [$table];
+ foreach ($indexes as $index) {
+ $out[] = $index;
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function truncateTableSql(TableSchema $schema): array
+ {
+ $name = $schema->name();
+ $sql = [];
+ if ($this->hasSequences()) {
+ $sql[] = sprintf('DELETE FROM sqlite_sequence WHERE name="%s"', $name);
+ }
+
+ $sql[] = sprintf('DELETE FROM "%s"', $name);
+
+ return $sql;
+ }
+
+ /**
+ * Returns whether there is any table in this connection to SQLite containing
+ * sequences
+ *
+ * @return bool
+ */
+ public function hasSequences(): bool
+ {
+ $result = $this->_driver->prepare(
+ 'SELECT 1 FROM sqlite_master WHERE name = "sqlite_sequence"'
+ );
+ $result->execute();
+ $this->_hasSequences = (bool)$result->rowCount();
+ $result->closeCursor();
+
+ return $this->_hasSequences;
+ }
+}
+
+// phpcs:disable
+// Add backwards compatible alias.
+class_alias('Cake\Database\Schema\SqliteSchemaDialect', 'Cake\Database\Schema\SqliteSchema');
+// phpcs:enable
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/SqlserverSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlserverSchema.php
new file mode 100644
index 000000000..dbe584efd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/SqlserverSchema.php
@@ -0,0 +1,5 @@
+ $col, 'length' => null];
+ }
+
+ if ($col === 'datetime') {
+ // datetime cannot parse more than 3 digits of precision and isn't accurate
+ return ['type' => TableSchema::TYPE_DATETIME, 'length' => null];
+ }
+ if (strpos($col, 'datetime') !== false) {
+ $typeName = TableSchema::TYPE_DATETIME;
+ if ($scale > 0) {
+ $typeName = TableSchema::TYPE_DATETIME_FRACTIONAL;
+ }
+
+ return ['type' => $typeName, 'length' => null, 'precision' => $scale];
+ }
+
+ if ($col === 'char') {
+ return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
+ }
+
+ if ($col === 'tinyint') {
+ return ['type' => TableSchema::TYPE_TINYINTEGER, 'length' => $precision ?: 3];
+ }
+ if ($col === 'smallint') {
+ return ['type' => TableSchema::TYPE_SMALLINTEGER, 'length' => $precision ?: 5];
+ }
+ if ($col === 'int' || $col === 'integer') {
+ return ['type' => TableSchema::TYPE_INTEGER, 'length' => $precision ?: 10];
+ }
+ if ($col === 'bigint') {
+ return ['type' => TableSchema::TYPE_BIGINTEGER, 'length' => $precision ?: 20];
+ }
+ if ($col === 'bit') {
+ return ['type' => TableSchema::TYPE_BOOLEAN, 'length' => null];
+ }
+ if (
+ strpos($col, 'numeric') !== false ||
+ strpos($col, 'money') !== false ||
+ strpos($col, 'decimal') !== false
+ ) {
+ return ['type' => TableSchema::TYPE_DECIMAL, 'length' => $precision, 'precision' => $scale];
+ }
+
+ if ($col === 'real' || $col === 'float') {
+ return ['type' => TableSchema::TYPE_FLOAT, 'length' => null];
+ }
+ // SqlServer schema reflection returns double length for unicode
+ // columns because internally it uses UTF16/UCS2
+ if ($col === 'nvarchar' || $col === 'nchar' || $col === 'ntext') {
+ $length /= 2;
+ }
+ if (strpos($col, 'varchar') !== false && $length < 0) {
+ return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
+ }
+
+ if (strpos($col, 'varchar') !== false) {
+ return ['type' => TableSchema::TYPE_STRING, 'length' => $length ?: 255];
+ }
+
+ if (strpos($col, 'char') !== false) {
+ return ['type' => TableSchema::TYPE_CHAR, 'length' => $length];
+ }
+
+ if (strpos($col, 'text') !== false) {
+ return ['type' => TableSchema::TYPE_TEXT, 'length' => null];
+ }
+
+ if ($col === 'image' || strpos($col, 'binary') !== false) {
+ // -1 is the value for MAX which we treat as a 'long' binary
+ if ($length == -1) {
+ $length = TableSchema::LENGTH_LONG;
+ }
+
+ return ['type' => TableSchema::TYPE_BINARY, 'length' => $length];
+ }
+
+ if ($col === 'uniqueidentifier') {
+ return ['type' => TableSchema::TYPE_UUID];
+ }
+
+ return ['type' => TableSchema::TYPE_STRING, 'length' => null];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertColumnDescription(TableSchema $schema, array $row): void
+ {
+ $field = $this->_convertColumn(
+ $row['type'],
+ $row['char_length'] !== null ? (int)$row['char_length'] : null,
+ $row['precision'] !== null ? (int)$row['precision'] : null,
+ $row['scale'] !== null ? (int)$row['scale'] : null
+ );
+
+ if (!empty($row['autoincrement'])) {
+ $field['autoIncrement'] = true;
+ }
+
+ $field += [
+ 'null' => $row['null'] === '1',
+ 'default' => $this->_defaultValue($field['type'], $row['default']),
+ 'collate' => $row['collation_name'],
+ ];
+ $schema->addColumn($row['name'], $field);
+ }
+
+ /**
+ * Manipulate the default value.
+ *
+ * Removes () wrapping default values, extracts strings from
+ * N'' wrappers and collation text and converts NULL strings.
+ *
+ * @param string $type The schema type
+ * @param string|null $default The default value.
+ * @return string|int|null
+ */
+ protected function _defaultValue($type, $default)
+ {
+ if ($default === null) {
+ return $default;
+ }
+
+ // remove () surrounding value (NULL) but leave () at the end of functions
+ // integers might have two ((0)) wrapping value
+ if (preg_match('/^\(+(.*?(\(\))?)\)+$/', $default, $matches)) {
+ $default = $matches[1];
+ }
+
+ if ($default === 'NULL') {
+ return null;
+ }
+
+ if ($type === TableSchema::TYPE_BOOLEAN) {
+ return (int)$default;
+ }
+
+ // Remove quotes
+ if (preg_match("/^\(?N?'(.*)'\)?/", $default, $matches)) {
+ return str_replace("''", "'", $matches[1]);
+ }
+
+ return $default;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeIndexSql(string $tableName, array $config): array
+ {
+ $sql = "SELECT
+ I.[name] AS [index_name],
+ IC.[index_column_id] AS [index_order],
+ AC.[name] AS [column_name],
+ I.[is_unique], I.[is_primary_key],
+ I.[is_unique_constraint]
+ FROM sys.[tables] AS T
+ INNER JOIN sys.[schemas] S ON S.[schema_id] = T.[schema_id]
+ INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id]
+ INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] AND I.[index_id] = IC.[index_id]
+ INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] AND IC.[column_id] = AC.[column_id]
+ WHERE T.[is_ms_shipped] = 0 AND I.[type_desc] <> 'HEAP' AND T.[name] = ? AND S.[name] = ?
+ ORDER BY I.[index_id], IC.[index_column_id]";
+
+ $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
+
+ return [$sql, [$tableName, $schema]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertIndexDescription(TableSchema $schema, array $row): void
+ {
+ $type = TableSchema::INDEX_INDEX;
+ $name = $row['index_name'];
+ if ($row['is_primary_key']) {
+ $name = $type = TableSchema::CONSTRAINT_PRIMARY;
+ }
+ if ($row['is_unique_constraint'] && $type === TableSchema::INDEX_INDEX) {
+ $type = TableSchema::CONSTRAINT_UNIQUE;
+ }
+
+ if ($type === TableSchema::INDEX_INDEX) {
+ $existing = $schema->getIndex($name);
+ } else {
+ $existing = $schema->getConstraint($name);
+ }
+
+ $columns = [$row['column_name']];
+ if (!empty($existing)) {
+ $columns = array_merge($existing['columns'], $columns);
+ }
+
+ if ($type === TableSchema::CONSTRAINT_PRIMARY || $type === TableSchema::CONSTRAINT_UNIQUE) {
+ $schema->addConstraint($name, [
+ 'type' => $type,
+ 'columns' => $columns,
+ ]);
+
+ return;
+ }
+ $schema->addIndex($name, [
+ 'type' => $type,
+ 'columns' => $columns,
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function describeForeignKeySql(string $tableName, array $config): array
+ {
+ // phpcs:disable Generic.Files.LineLength
+ $sql = 'SELECT FK.[name] AS [foreign_key_name], FK.[delete_referential_action_desc] AS [delete_type],
+ FK.[update_referential_action_desc] AS [update_type], C.name AS [column], RT.name AS [reference_table],
+ RC.name AS [reference_column]
+ FROM sys.foreign_keys FK
+ INNER JOIN sys.foreign_key_columns FKC ON FKC.constraint_object_id = FK.object_id
+ INNER JOIN sys.tables T ON T.object_id = FKC.parent_object_id
+ INNER JOIN sys.tables RT ON RT.object_id = FKC.referenced_object_id
+ INNER JOIN sys.schemas S ON S.schema_id = T.schema_id AND S.schema_id = RT.schema_id
+ INNER JOIN sys.columns C ON C.column_id = FKC.parent_column_id AND C.object_id = FKC.parent_object_id
+ INNER JOIN sys.columns RC ON RC.column_id = FKC.referenced_column_id AND RC.object_id = FKC.referenced_object_id
+ WHERE FK.is_ms_shipped = 0 AND T.name = ? AND S.name = ?';
+ // phpcs:enable Generic.Files.LineLength
+
+ $schema = empty($config['schema']) ? static::DEFAULT_SCHEMA_NAME : $config['schema'];
+
+ return [$sql, [$tableName, $schema]];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertForeignKeyDescription(TableSchema $schema, array $row): void
+ {
+ $data = [
+ 'type' => TableSchema::CONSTRAINT_FOREIGN,
+ 'columns' => [$row['column']],
+ 'references' => [$row['reference_table'], $row['reference_column']],
+ 'update' => $this->_convertOnClause($row['update_type']),
+ 'delete' => $this->_convertOnClause($row['delete_type']),
+ ];
+ $name = $row['foreign_key_name'];
+ $schema->addConstraint($name, $data);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _foreignOnClause(string $on): string
+ {
+ $parent = parent::_foreignOnClause($on);
+
+ return $parent === 'RESTRICT' ? parent::_foreignOnClause(TableSchema::ACTION_NO_ACTION) : $parent;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _convertOnClause(string $clause): string
+ {
+ switch ($clause) {
+ case 'NO_ACTION':
+ return TableSchema::ACTION_NO_ACTION;
+ case 'CASCADE':
+ return TableSchema::ACTION_CASCADE;
+ case 'SET_NULL':
+ return TableSchema::ACTION_SET_NULL;
+ case 'SET_DEFAULT':
+ return TableSchema::ACTION_SET_DEFAULT;
+ }
+
+ return TableSchema::ACTION_SET_NULL;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function columnSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getColumn($name);
+ $out = $this->_driver->quoteIdentifier($name);
+ $typeMap = [
+ TableSchema::TYPE_TINYINTEGER => ' TINYINT',
+ TableSchema::TYPE_SMALLINTEGER => ' SMALLINT',
+ TableSchema::TYPE_INTEGER => ' INTEGER',
+ TableSchema::TYPE_BIGINTEGER => ' BIGINT',
+ TableSchema::TYPE_BINARY_UUID => ' UNIQUEIDENTIFIER',
+ TableSchema::TYPE_BOOLEAN => ' BIT',
+ TableSchema::TYPE_CHAR => ' NCHAR',
+ TableSchema::TYPE_FLOAT => ' FLOAT',
+ TableSchema::TYPE_DECIMAL => ' DECIMAL',
+ TableSchema::TYPE_DATE => ' DATE',
+ TableSchema::TYPE_TIME => ' TIME',
+ TableSchema::TYPE_DATETIME => ' DATETIME2',
+ TableSchema::TYPE_DATETIME_FRACTIONAL => ' DATETIME2',
+ TableSchema::TYPE_TIMESTAMP => ' DATETIME2',
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL => ' DATETIME2',
+ TableSchema::TYPE_TIMESTAMP_TIMEZONE => ' DATETIME2',
+ TableSchema::TYPE_UUID => ' UNIQUEIDENTIFIER',
+ TableSchema::TYPE_JSON => ' NVARCHAR(MAX)',
+ ];
+
+ if (isset($typeMap[$data['type']])) {
+ $out .= $typeMap[$data['type']];
+ }
+
+ if ($data['type'] === TableSchema::TYPE_INTEGER || $data['type'] === TableSchema::TYPE_BIGINTEGER) {
+ if ($schema->getPrimaryKey() === [$name] || $data['autoIncrement'] === true) {
+ unset($data['null'], $data['default']);
+ $out .= ' IDENTITY(1, 1)';
+ }
+ }
+
+ if ($data['type'] === TableSchema::TYPE_TEXT && $data['length'] !== TableSchema::LENGTH_TINY) {
+ $out .= ' NVARCHAR(MAX)';
+ }
+
+ if ($data['type'] === TableSchema::TYPE_CHAR) {
+ $out .= '(' . $data['length'] . ')';
+ }
+
+ if ($data['type'] === TableSchema::TYPE_BINARY) {
+ if (
+ !isset($data['length'])
+ || in_array($data['length'], [TableSchema::LENGTH_MEDIUM, TableSchema::LENGTH_LONG], true)
+ ) {
+ $data['length'] = 'MAX';
+ }
+
+ if ($data['length'] === 1) {
+ $out .= ' BINARY(1)';
+ } else {
+ $out .= ' VARBINARY';
+
+ $out .= sprintf('(%s)', $data['length']);
+ }
+ }
+
+ if (
+ $data['type'] === TableSchema::TYPE_STRING ||
+ (
+ $data['type'] === TableSchema::TYPE_TEXT &&
+ $data['length'] === TableSchema::LENGTH_TINY
+ )
+ ) {
+ $type = ' NVARCHAR';
+ $length = $data['length'] ?? TableSchema::LENGTH_TINY;
+ $out .= sprintf('%s(%d)', $type, $length);
+ }
+
+ $hasCollate = [TableSchema::TYPE_TEXT, TableSchema::TYPE_STRING, TableSchema::TYPE_CHAR];
+ if (in_array($data['type'], $hasCollate, true) && isset($data['collate']) && $data['collate'] !== '') {
+ $out .= ' COLLATE ' . $data['collate'];
+ }
+
+ $precisionTypes = [
+ TableSchema::TYPE_FLOAT,
+ TableSchema::TYPE_DATETIME,
+ TableSchema::TYPE_DATETIME_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ ];
+ if (in_array($data['type'], $precisionTypes, true) && isset($data['precision'])) {
+ $out .= '(' . (int)$data['precision'] . ')';
+ }
+
+ if (
+ $data['type'] === TableSchema::TYPE_DECIMAL &&
+ (
+ isset($data['length']) ||
+ isset($data['precision'])
+ )
+ ) {
+ $out .= '(' . (int)$data['length'] . ',' . (int)$data['precision'] . ')';
+ }
+
+ if (isset($data['null']) && $data['null'] === false) {
+ $out .= ' NOT NULL';
+ }
+
+ $dateTimeTypes = [
+ TableSchema::TYPE_DATETIME,
+ TableSchema::TYPE_DATETIME_FRACTIONAL,
+ TableSchema::TYPE_TIMESTAMP,
+ TableSchema::TYPE_TIMESTAMP_FRACTIONAL,
+ ];
+ $dateTimeDefaults = [
+ 'current_timestamp',
+ 'getdate()',
+ 'getutcdate()',
+ 'sysdatetime()',
+ 'sysutcdatetime()',
+ 'sysdatetimeoffset()',
+ ];
+ if (
+ isset($data['default']) &&
+ in_array($data['type'], $dateTimeTypes, true) &&
+ in_array(strtolower($data['default']), $dateTimeDefaults, true)
+ ) {
+ $out .= ' DEFAULT ' . strtoupper($data['default']);
+ } elseif (isset($data['default'])) {
+ $default = is_bool($data['default'])
+ ? (int)$data['default']
+ : $this->_driver->schemaValue($data['default']);
+ $out .= ' DEFAULT ' . $default;
+ } elseif (isset($data['null']) && $data['null'] !== false) {
+ $out .= ' DEFAULT NULL';
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addConstraintSql(TableSchema $schema): array
+ {
+ $sqlPattern = 'ALTER TABLE %s ADD %s;';
+ $sql = [];
+
+ foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
+ $constraint = $schema->getConstraint($name);
+ if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $sql[] = sprintf($sqlPattern, $tableName, $this->constraintSql($schema, $name));
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropConstraintSql(TableSchema $schema): array
+ {
+ $sqlPattern = 'ALTER TABLE %s DROP CONSTRAINT %s;';
+ $sql = [];
+
+ foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
+ $constraint = $schema->getConstraint($name);
+ if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $constraintName = $this->_driver->quoteIdentifier($name);
+ $sql[] = sprintf($sqlPattern, $tableName, $constraintName);
+ }
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function indexSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getIndex($name);
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+
+ return sprintf(
+ 'CREATE INDEX %s ON %s (%s)',
+ $this->_driver->quoteIdentifier($name),
+ $this->_driver->quoteIdentifier($schema->name()),
+ implode(', ', $columns)
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function constraintSql(TableSchema $schema, string $name): string
+ {
+ /** @var array $data */
+ $data = $schema->getConstraint($name);
+ $out = 'CONSTRAINT ' . $this->_driver->quoteIdentifier($name);
+ if ($data['type'] === TableSchema::CONSTRAINT_PRIMARY) {
+ $out = 'PRIMARY KEY';
+ }
+ if ($data['type'] === TableSchema::CONSTRAINT_UNIQUE) {
+ $out .= ' UNIQUE';
+ }
+
+ return $this->_keySql($out, $data);
+ }
+
+ /**
+ * Helper method for generating key SQL snippets.
+ *
+ * @param string $prefix The key prefix
+ * @param array $data Key data.
+ * @return string
+ */
+ protected function _keySql(string $prefix, array $data): string
+ {
+ $columns = array_map(
+ [$this->_driver, 'quoteIdentifier'],
+ $data['columns']
+ );
+ if ($data['type'] === TableSchema::CONSTRAINT_FOREIGN) {
+ return $prefix . sprintf(
+ ' FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s',
+ implode(', ', $columns),
+ $this->_driver->quoteIdentifier($data['references'][0]),
+ $this->_convertConstraintColumns($data['references'][1]),
+ $this->_foreignOnClause($data['update']),
+ $this->_foreignOnClause($data['delete'])
+ );
+ }
+
+ return $prefix . ' (' . implode(', ', $columns) . ')';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createTableSql(TableSchema $schema, array $columns, array $constraints, array $indexes): array
+ {
+ $content = array_merge($columns, $constraints);
+ $content = implode(",\n", array_filter($content));
+ $tableName = $this->_driver->quoteIdentifier($schema->name());
+ $out = [];
+ $out[] = sprintf("CREATE TABLE %s (\n%s\n)", $tableName, $content);
+ foreach ($indexes as $index) {
+ $out[] = $index;
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function truncateTableSql(TableSchema $schema): array
+ {
+ $name = $this->_driver->quoteIdentifier($schema->name());
+ $queries = [
+ sprintf('DELETE FROM %s', $name),
+ ];
+
+ // Restart identity sequences
+ $pk = $schema->getPrimaryKey();
+ if (count($pk) === 1) {
+ /** @var array $column */
+ $column = $schema->getColumn($pk[0]);
+ if (in_array($column['type'], ['integer', 'biginteger'])) {
+ $queries[] = sprintf(
+ "DBCC CHECKIDENT('%s', RESEED, 0)",
+ $schema->name()
+ );
+ }
+ }
+
+ return $queries;
+ }
+}
+
+// phpcs:disable
+// Add backwards compatible alias.
+class_alias('Cake\Database\Schema\SqlserverSchemaDialect', 'Cake\Database\Schema\SqlserverSchema');
+// phpcs:enable
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchema.php b/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchema.php
new file mode 100644
index 000000000..4c43492a8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchema.php
@@ -0,0 +1,803 @@
+ self::LENGTH_TINY,
+ 'medium' => self::LENGTH_MEDIUM,
+ 'long' => self::LENGTH_LONG,
+ ];
+
+ /**
+ * The valid keys that can be used in a column
+ * definition.
+ *
+ * @var array
+ */
+ protected static $_columnKeys = [
+ 'type' => null,
+ 'baseType' => null,
+ 'length' => null,
+ 'precision' => null,
+ 'null' => null,
+ 'default' => null,
+ 'comment' => null,
+ ];
+
+ /**
+ * Additional type specific properties.
+ *
+ * @var array
+ */
+ protected static $_columnExtras = [
+ 'string' => [
+ 'collate' => null,
+ ],
+ 'char' => [
+ 'collate' => null,
+ ],
+ 'text' => [
+ 'collate' => null,
+ ],
+ 'tinyinteger' => [
+ 'unsigned' => null,
+ ],
+ 'smallinteger' => [
+ 'unsigned' => null,
+ ],
+ 'integer' => [
+ 'unsigned' => null,
+ 'autoIncrement' => null,
+ ],
+ 'biginteger' => [
+ 'unsigned' => null,
+ 'autoIncrement' => null,
+ ],
+ 'decimal' => [
+ 'unsigned' => null,
+ ],
+ 'float' => [
+ 'unsigned' => null,
+ ],
+ ];
+
+ /**
+ * The valid keys that can be used in an index
+ * definition.
+ *
+ * @var array
+ */
+ protected static $_indexKeys = [
+ 'type' => null,
+ 'columns' => [],
+ 'length' => [],
+ 'references' => [],
+ 'update' => 'restrict',
+ 'delete' => 'restrict',
+ ];
+
+ /**
+ * Names of the valid index types.
+ *
+ * @var array
+ */
+ protected static $_validIndexTypes = [
+ self::INDEX_INDEX,
+ self::INDEX_FULLTEXT,
+ ];
+
+ /**
+ * Names of the valid constraint types.
+ *
+ * @var array
+ */
+ protected static $_validConstraintTypes = [
+ self::CONSTRAINT_PRIMARY,
+ self::CONSTRAINT_UNIQUE,
+ self::CONSTRAINT_FOREIGN,
+ ];
+
+ /**
+ * Names of the valid foreign key actions.
+ *
+ * @var array
+ */
+ protected static $_validForeignKeyActions = [
+ self::ACTION_CASCADE,
+ self::ACTION_SET_NULL,
+ self::ACTION_SET_DEFAULT,
+ self::ACTION_NO_ACTION,
+ self::ACTION_RESTRICT,
+ ];
+
+ /**
+ * Primary constraint type
+ *
+ * @var string
+ */
+ public const CONSTRAINT_PRIMARY = 'primary';
+
+ /**
+ * Unique constraint type
+ *
+ * @var string
+ */
+ public const CONSTRAINT_UNIQUE = 'unique';
+
+ /**
+ * Foreign constraint type
+ *
+ * @var string
+ */
+ public const CONSTRAINT_FOREIGN = 'foreign';
+
+ /**
+ * Index - index type
+ *
+ * @var string
+ */
+ public const INDEX_INDEX = 'index';
+
+ /**
+ * Fulltext index type
+ *
+ * @var string
+ */
+ public const INDEX_FULLTEXT = 'fulltext';
+
+ /**
+ * Foreign key cascade action
+ *
+ * @var string
+ */
+ public const ACTION_CASCADE = 'cascade';
+
+ /**
+ * Foreign key set null action
+ *
+ * @var string
+ */
+ public const ACTION_SET_NULL = 'setNull';
+
+ /**
+ * Foreign key no action
+ *
+ * @var string
+ */
+ public const ACTION_NO_ACTION = 'noAction';
+
+ /**
+ * Foreign key restrict action
+ *
+ * @var string
+ */
+ public const ACTION_RESTRICT = 'restrict';
+
+ /**
+ * Foreign key restrict default
+ *
+ * @var string
+ */
+ public const ACTION_SET_DEFAULT = 'setDefault';
+
+ /**
+ * Constructor.
+ *
+ * @param string $table The table name.
+ * @param array $columns The list of columns for the schema.
+ */
+ public function __construct(string $table, array $columns = [])
+ {
+ $this->_table = $table;
+ foreach ($columns as $field => $definition) {
+ $this->addColumn($field, $definition);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function name(): string
+ {
+ return $this->_table;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addColumn(string $name, $attrs)
+ {
+ if (is_string($attrs)) {
+ $attrs = ['type' => $attrs];
+ }
+ $valid = static::$_columnKeys;
+ if (isset(static::$_columnExtras[$attrs['type']])) {
+ $valid += static::$_columnExtras[$attrs['type']];
+ }
+ $attrs = array_intersect_key($attrs, $valid);
+ $this->_columns[$name] = $attrs + $valid;
+ $this->_typeMap[$name] = $this->_columns[$name]['type'];
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function removeColumn(string $name)
+ {
+ unset($this->_columns[$name], $this->_typeMap[$name]);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function columns(): array
+ {
+ return array_keys($this->_columns);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getColumn(string $name): ?array
+ {
+ if (!isset($this->_columns[$name])) {
+ return null;
+ }
+ $column = $this->_columns[$name];
+ unset($column['baseType']);
+
+ return $column;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getColumnType(string $name): ?string
+ {
+ if (!isset($this->_columns[$name])) {
+ return null;
+ }
+
+ return $this->_columns[$name]['type'];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setColumnType(string $name, string $type)
+ {
+ if (!isset($this->_columns[$name])) {
+ return $this;
+ }
+
+ $this->_columns[$name]['type'] = $type;
+ $this->_typeMap[$name] = $type;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasColumn(string $name): bool
+ {
+ return isset($this->_columns[$name]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function baseColumnType(string $column): ?string
+ {
+ if (isset($this->_columns[$column]['baseType'])) {
+ return $this->_columns[$column]['baseType'];
+ }
+
+ $type = $this->getColumnType($column);
+
+ if ($type === null) {
+ return null;
+ }
+
+ if (TypeFactory::getMap($type)) {
+ $type = TypeFactory::build($type)->getBaseType();
+ }
+
+ return $this->_columns[$column]['baseType'] = $type;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function typeMap(): array
+ {
+ return $this->_typeMap;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isNullable(string $name): bool
+ {
+ if (!isset($this->_columns[$name])) {
+ return true;
+ }
+
+ return $this->_columns[$name]['null'] === true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function defaultValues(): array
+ {
+ $defaults = [];
+ foreach ($this->_columns as $name => $data) {
+ if (!array_key_exists('default', $data)) {
+ continue;
+ }
+ if ($data['default'] === null && $data['null'] !== true) {
+ continue;
+ }
+ $defaults[$name] = $data['default'];
+ }
+
+ return $defaults;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addIndex(string $name, $attrs)
+ {
+ if (is_string($attrs)) {
+ $attrs = ['type' => $attrs];
+ }
+ $attrs = array_intersect_key($attrs, static::$_indexKeys);
+ $attrs += static::$_indexKeys;
+ unset($attrs['references'], $attrs['update'], $attrs['delete']);
+
+ if (!in_array($attrs['type'], static::$_validIndexTypes, true)) {
+ throw new DatabaseException(sprintf(
+ 'Invalid index type "%s" in index "%s" in table "%s".',
+ $attrs['type'],
+ $name,
+ $this->_table
+ ));
+ }
+ if (empty($attrs['columns'])) {
+ throw new DatabaseException(sprintf(
+ 'Index "%s" in table "%s" must have at least one column.',
+ $name,
+ $this->_table
+ ));
+ }
+ $attrs['columns'] = (array)$attrs['columns'];
+ foreach ($attrs['columns'] as $field) {
+ if (empty($this->_columns[$field])) {
+ $msg = sprintf(
+ 'Columns used in index "%s" in table "%s" must be added to the Table schema first. ' .
+ 'The column "%s" was not found.',
+ $name,
+ $this->_table,
+ $field
+ );
+ throw new DatabaseException($msg);
+ }
+ }
+ $this->_indexes[$name] = $attrs;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function indexes(): array
+ {
+ return array_keys($this->_indexes);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getIndex(string $name): ?array
+ {
+ if (!isset($this->_indexes[$name])) {
+ return null;
+ }
+
+ return $this->_indexes[$name];
+ }
+
+ /**
+ * Get the column(s) used for the primary key.
+ *
+ * @return array Column name(s) for the primary key. An
+ * empty list will be returned when the table has no primary key.
+ * @deprecated 4.0.0 Renamed to {@link getPrimaryKey()}.
+ */
+ public function primaryKey(): array
+ {
+ deprecationWarning('`TableSchema::primaryKey()` is deprecated. Use `TableSchema::getPrimaryKey()`.');
+
+ return $this->getPrimarykey();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPrimaryKey(): array
+ {
+ foreach ($this->_constraints as $data) {
+ if ($data['type'] === static::CONSTRAINT_PRIMARY) {
+ return $data['columns'];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addConstraint(string $name, $attrs)
+ {
+ if (is_string($attrs)) {
+ $attrs = ['type' => $attrs];
+ }
+ $attrs = array_intersect_key($attrs, static::$_indexKeys);
+ $attrs += static::$_indexKeys;
+ if (!in_array($attrs['type'], static::$_validConstraintTypes, true)) {
+ throw new DatabaseException(sprintf(
+ 'Invalid constraint type "%s" in table "%s".',
+ $attrs['type'],
+ $this->_table
+ ));
+ }
+ if (empty($attrs['columns'])) {
+ throw new DatabaseException(sprintf(
+ 'Constraints in table "%s" must have at least one column.',
+ $this->_table
+ ));
+ }
+ $attrs['columns'] = (array)$attrs['columns'];
+ foreach ($attrs['columns'] as $field) {
+ if (empty($this->_columns[$field])) {
+ $msg = sprintf(
+ 'Columns used in constraints must be added to the Table schema first. ' .
+ 'The column "%s" was not found in table "%s".',
+ $field,
+ $this->_table
+ );
+ throw new DatabaseException($msg);
+ }
+ }
+
+ if ($attrs['type'] === static::CONSTRAINT_FOREIGN) {
+ $attrs = $this->_checkForeignKey($attrs);
+
+ if (isset($this->_constraints[$name])) {
+ $this->_constraints[$name]['columns'] = array_unique(array_merge(
+ $this->_constraints[$name]['columns'],
+ $attrs['columns']
+ ));
+
+ if (isset($this->_constraints[$name]['references'])) {
+ $this->_constraints[$name]['references'][1] = array_unique(array_merge(
+ (array)$this->_constraints[$name]['references'][1],
+ [$attrs['references'][1]]
+ ));
+ }
+
+ return $this;
+ }
+ } else {
+ unset($attrs['references'], $attrs['update'], $attrs['delete']);
+ }
+
+ $this->_constraints[$name] = $attrs;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropConstraint(string $name)
+ {
+ if (isset($this->_constraints[$name])) {
+ unset($this->_constraints[$name]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Check whether or not a table has an autoIncrement column defined.
+ *
+ * @return bool
+ */
+ public function hasAutoincrement(): bool
+ {
+ foreach ($this->_columns as $column) {
+ if (isset($column['autoIncrement']) && $column['autoIncrement']) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to check/validate foreign keys.
+ *
+ * @param array $attrs Attributes to set.
+ * @return array
+ * @throws \Cake\Database\Exception\DatabaseException When foreign key definition is not valid.
+ */
+ protected function _checkForeignKey(array $attrs): array
+ {
+ if (count($attrs['references']) < 2) {
+ throw new DatabaseException('References must contain a table and column.');
+ }
+ if (!in_array($attrs['update'], static::$_validForeignKeyActions)) {
+ throw new DatabaseException(sprintf(
+ 'Update action is invalid. Must be one of %s',
+ implode(',', static::$_validForeignKeyActions)
+ ));
+ }
+ if (!in_array($attrs['delete'], static::$_validForeignKeyActions)) {
+ throw new DatabaseException(sprintf(
+ 'Delete action is invalid. Must be one of %s',
+ implode(',', static::$_validForeignKeyActions)
+ ));
+ }
+
+ return $attrs;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function constraints(): array
+ {
+ return array_keys($this->_constraints);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConstraint(string $name): ?array
+ {
+ if (!isset($this->_constraints[$name])) {
+ return null;
+ }
+
+ return $this->_constraints[$name];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setOptions(array $options)
+ {
+ $this->_options = array_merge($this->_options, $options);
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getOptions(): array
+ {
+ return $this->_options;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setTemporary(bool $temporary)
+ {
+ $this->_temporary = $temporary;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isTemporary(): bool
+ {
+ return $this->_temporary;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createSql(Connection $connection): array
+ {
+ $dialect = $connection->getDriver()->schemaDialect();
+ $columns = $constraints = $indexes = [];
+ foreach (array_keys($this->_columns) as $name) {
+ $columns[] = $dialect->columnSql($this, $name);
+ }
+ foreach (array_keys($this->_constraints) as $name) {
+ $constraints[] = $dialect->constraintSql($this, $name);
+ }
+ foreach (array_keys($this->_indexes) as $name) {
+ $indexes[] = $dialect->indexSql($this, $name);
+ }
+
+ return $dialect->createTableSql($this, $columns, $constraints, $indexes);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropSql(Connection $connection): array
+ {
+ $dialect = $connection->getDriver()->schemaDialect();
+
+ return $dialect->dropTableSql($this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function truncateSql(Connection $connection): array
+ {
+ $dialect = $connection->getDriver()->schemaDialect();
+
+ return $dialect->truncateTableSql($this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addConstraintSql(Connection $connection): array
+ {
+ $dialect = $connection->getDriver()->schemaDialect();
+
+ return $dialect->addConstraintSql($this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropConstraintSql(Connection $connection): array
+ {
+ $dialect = $connection->getDriver()->schemaDialect();
+
+ return $dialect->dropConstraintSql($this);
+ }
+
+ /**
+ * Returns an array of the table schema.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'table' => $this->_table,
+ 'columns' => $this->_columns,
+ 'indexes' => $this->_indexes,
+ 'constraints' => $this->_constraints,
+ 'options' => $this->_options,
+ 'typeMap' => $this->_typeMap,
+ 'temporary' => $this->_temporary,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchemaAwareInterface.php b/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchemaAwareInterface.php
new file mode 100644
index 000000000..f08e363ba
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Schema/TableSchemaAwareInterface.php
@@ -0,0 +1,38 @@
+_schema = $this->getSchema($connection);
+ }
+
+ /**
+ * Build metadata.
+ *
+ * @param string|null $name The name of the table to build cache data for.
+ * @return array Returns a list build table caches
+ */
+ public function build(?string $name = null): array
+ {
+ $tables = [$name];
+ if (empty($name)) {
+ $tables = $this->_schema->listTables();
+ }
+
+ foreach ($tables as $table) {
+ /** @psalm-suppress PossiblyNullArgument */
+ $this->_schema->describe($table, ['forceRefresh' => true]);
+ }
+
+ return $tables;
+ }
+
+ /**
+ * Clear metadata.
+ *
+ * @param string|null $name The name of the table to clear cache data for.
+ * @return array Returns a list of cleared table caches
+ */
+ public function clear(?string $name = null): array
+ {
+ $tables = [$name];
+ if (empty($name)) {
+ $tables = $this->_schema->listTables();
+ }
+
+ $cacher = $this->_schema->getCacher();
+
+ foreach ($tables as $table) {
+ /** @psalm-suppress PossiblyNullArgument */
+ $key = $this->_schema->cacheKey($table);
+ $cacher->delete($key);
+ }
+
+ return $tables;
+ }
+
+ /**
+ * Helper method to get the schema collection.
+ *
+ * @param \Cake\Database\Connection $connection Connection object
+ * @return \Cake\Database\Schema\CachedCollection
+ * @throws \RuntimeException If given connection object is not compatible with schema caching
+ */
+ public function getSchema(Connection $connection): CachedCollection
+ {
+ $config = $connection->config();
+ if (empty($config['cacheMetadata'])) {
+ $connection->cacheMetadata(true);
+ }
+
+ /** @var \Cake\Database\Schema\CachedCollection $schemaCollection */
+ $schemaCollection = $connection->getSchemaCollection();
+
+ return $schemaCollection;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/SqlDialectTrait.php b/app/vendor/cakephp/cakephp/src/Database/SqlDialectTrait.php
new file mode 100644
index 000000000..38ef7bc08
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/SqlDialectTrait.php
@@ -0,0 +1,5 @@
+ 'DELETE',
+ 'where' => ' WHERE %s',
+ 'group' => ' GROUP BY %s',
+ 'order' => ' %s',
+ 'offset' => ' OFFSET %s ROWS',
+ 'epilog' => ' %s',
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ protected $_selectParts = [
+ 'with', 'select', 'from', 'join', 'where', 'group', 'having', 'window', 'order',
+ 'offset', 'limit', 'union', 'epilog',
+ ];
+
+ /**
+ * Helper function used to build the string representation of a `WITH` clause,
+ * it constructs the CTE definitions list without generating the `RECURSIVE`
+ * keyword that is neither required nor valid.
+ *
+ * @param array $parts List of CTEs to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildWithPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ $expressions = [];
+ foreach ($parts as $cte) {
+ $expressions[] = $cte->sql($binder);
+ }
+
+ return sprintf('WITH %s ', implode(', ', $expressions));
+ }
+
+ /**
+ * Generates the INSERT part of a SQL query
+ *
+ * To better handle concurrency and low transaction isolation levels,
+ * we also include an OUTPUT clause so we can ensure we get the inserted
+ * row's data back.
+ *
+ * @param array $parts The parts to build
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildInsertPart(array $parts, Query $query, ValueBinder $binder): string
+ {
+ if (!isset($parts[0])) {
+ throw new DatabaseException(
+ 'Could not compile insert query. No table was specified. ' .
+ 'Use `into()` to define a table.'
+ );
+ }
+ $table = $parts[0];
+ $columns = $this->_stringifyExpressions($parts[1], $binder);
+ $modifiers = $this->_buildModifierPart($query->clause('modifier'), $query, $binder);
+
+ return sprintf(
+ 'INSERT%s INTO %s (%s) OUTPUT INSERTED.*',
+ $modifiers,
+ $table,
+ implode(', ', $columns)
+ );
+ }
+
+ /**
+ * Generates the LIMIT part of a SQL query
+ *
+ * @param int $limit the limit clause
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @return string
+ */
+ protected function _buildLimitPart(int $limit, Query $query): string
+ {
+ if ($query->clause('offset') === null) {
+ return '';
+ }
+
+ return sprintf(' FETCH FIRST %d ROWS ONLY', $limit);
+ }
+
+ /**
+ * Helper function used to build the string representation of a HAVING clause,
+ * it constructs the field list taking care of aliasing and
+ * converting expression objects to string.
+ *
+ * @param array $parts list of fields to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @param \Cake\Database\ValueBinder $binder Value binder used to generate parameter placeholder
+ * @return string
+ */
+ protected function _buildHavingPart($parts, $query, $binder)
+ {
+ $selectParts = $query->clause('select');
+
+ foreach ($selectParts as $selectKey => $selectPart) {
+ if (!$selectPart instanceof FunctionExpression) {
+ continue;
+ }
+ foreach ($parts as $k => $p) {
+ if (!is_string($p)) {
+ continue;
+ }
+ preg_match_all(
+ '/\b' . trim($selectKey, '[]') . '\b/i',
+ $p,
+ $matches
+ );
+
+ if (empty($matches[0])) {
+ continue;
+ }
+
+ $parts[$k] = preg_replace(
+ ['/\[|\]/', '/\b' . trim($selectKey, '[]') . '\b/i'],
+ ['', $selectPart->sql($binder)],
+ $p
+ );
+ }
+ }
+
+ return sprintf(' HAVING %s', implode(', ', $parts));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/BufferResultsTrait.php b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferResultsTrait.php
new file mode 100644
index 000000000..e4e690f33
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferResultsTrait.php
@@ -0,0 +1,45 @@
+_bufferResults = $buffer;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/BufferedStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferedStatement.php
new file mode 100644
index 000000000..d4a009094
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/BufferedStatement.php
@@ -0,0 +1,345 @@
+statement = $statement;
+ $this->_driver = $driver;
+ }
+
+ /**
+ * Magic getter to return $queryString as read-only.
+ *
+ * @param string $property internal property to get
+ * @return mixed
+ */
+ public function __get(string $property)
+ {
+ if ($property === 'queryString') {
+ /** @psalm-suppress NoInterfaceProperties */
+ return $this->statement->queryString;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function bindValue($column, $value, $type = 'string'): void
+ {
+ $this->statement->bindValue($column, $value, $type);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function closeCursor(): void
+ {
+ $this->statement->closeCursor();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function columnCount(): int
+ {
+ return $this->statement->columnCount();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function errorCode()
+ {
+ return $this->statement->errorCode();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function errorInfo(): array
+ {
+ return $this->statement->errorInfo();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(?array $params = null): bool
+ {
+ $this->_reset();
+ $this->_hasExecuted = true;
+
+ return $this->statement->execute($params);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetchColumn(int $position)
+ {
+ $result = $this->fetch(static::FETCH_TYPE_NUM);
+ if ($result !== false && isset($result[$position])) {
+ return $result[$position];
+ }
+
+ return false;
+ }
+
+ /**
+ * Statements can be passed as argument for count() to return the number
+ * for affected rows from last execution.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->rowCount();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function bind(array $params, array $types): void
+ {
+ $this->statement->bind($params, $types);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function lastInsertId(?string $table = null, ?string $column = null)
+ {
+ return $this->statement->lastInsertId($table, $column);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param int|string $type The type to fetch.
+ * @return array|false
+ */
+ public function fetch($type = self::FETCH_TYPE_NUM)
+ {
+ if ($this->_allFetched) {
+ $row = false;
+ if (isset($this->buffer[$this->index])) {
+ $row = $this->buffer[$this->index];
+ }
+ $this->index += 1;
+
+ if ($row && $type === static::FETCH_TYPE_NUM) {
+ return array_values($row);
+ }
+
+ return $row;
+ }
+
+ $record = $this->statement->fetch($type);
+ if ($record === false) {
+ $this->_allFetched = true;
+ $this->statement->closeCursor();
+
+ return false;
+ }
+ $this->buffer[] = $record;
+
+ return $record;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetchAssoc(): array
+ {
+ $result = $this->fetch(static::FETCH_TYPE_ASSOC);
+
+ return $result ?: [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fetchAll($type = self::FETCH_TYPE_NUM)
+ {
+ if ($this->_allFetched) {
+ return $this->buffer;
+ }
+ $results = $this->statement->fetchAll($type);
+ if ($results !== false) {
+ $this->buffer = array_merge($this->buffer, $results);
+ }
+ $this->_allFetched = true;
+ $this->statement->closeCursor();
+
+ return $this->buffer;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function rowCount(): int
+ {
+ if (!$this->_allFetched) {
+ $this->fetchAll(static::FETCH_TYPE_ASSOC);
+ }
+
+ return count($this->buffer);
+ }
+
+ /**
+ * Reset all properties
+ *
+ * @return void
+ */
+ protected function _reset(): void
+ {
+ $this->buffer = [];
+ $this->_allFetched = false;
+ $this->index = 0;
+ }
+
+ /**
+ * Returns the current key in the iterator
+ *
+ * @return mixed
+ */
+ public function key()
+ {
+ return $this->index;
+ }
+
+ /**
+ * Returns the current record in the iterator
+ *
+ * @return mixed
+ */
+ public function current()
+ {
+ return $this->buffer[$this->index];
+ }
+
+ /**
+ * Rewinds the collection
+ *
+ * @return void
+ */
+ public function rewind(): void
+ {
+ $this->index = 0;
+ }
+
+ /**
+ * Returns whether or not the iterator has more elements
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ $old = $this->index;
+ $row = $this->fetch(self::FETCH_TYPE_ASSOC);
+
+ // Restore the index as fetch() increments during
+ // the cache scenario.
+ $this->index = $old;
+
+ return $row !== false;
+ }
+
+ /**
+ * Advances the iterator pointer to the next element
+ *
+ * @return void
+ */
+ public function next(): void
+ {
+ $this->index += 1;
+ }
+
+ /**
+ * Get the wrapped statement
+ *
+ * @return \Cake\Database\StatementInterface
+ */
+ public function getInnerStatement(): StatementInterface
+ {
+ return $this->statement;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/CallbackStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/CallbackStatement.php
new file mode 100644
index 000000000..692a2aa26
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/CallbackStatement.php
@@ -0,0 +1,79 @@
+_callback = $callback;
+ }
+
+ /**
+ * Fetch a row from the statement.
+ *
+ * The result will be processed by the callback when it is not `false`.
+ *
+ * @param string|int $type Either 'num' or 'assoc' to indicate the result format you would like.
+ * @return array|false
+ */
+ public function fetch($type = parent::FETCH_TYPE_NUM)
+ {
+ $callback = $this->_callback;
+ $row = $this->_statement->fetch($type);
+
+ return $row === false ? $row : $callback($row);
+ }
+
+ /**
+ * Fetch all rows from the statement.
+ *
+ * Each row in the result will be processed by the callback when it is not `false.
+ *
+ * @param string|int $type Either 'num' or 'assoc' to indicate the result format you would like.
+ * @return array
+ */
+ public function fetchAll($type = parent::FETCH_TYPE_NUM): array
+ {
+ /** @psalm-suppress PossiblyFalseArgument */
+ return array_map($this->_callback, $this->_statement->fetchAll($type));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php
new file mode 100644
index 000000000..2ad4ec008
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/MysqlStatement.php
@@ -0,0 +1,46 @@
+_driver->getConnection();
+
+ try {
+ $connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $this->_bufferResults);
+ $result = $this->_statement->execute($params);
+ } finally {
+ $connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/PDOStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/PDOStatement.php
new file mode 100644
index 000000000..fbc8349da
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/PDOStatement.php
@@ -0,0 +1,160 @@
+_statement = $statement;
+ $this->_driver = $driver;
+ }
+
+ /**
+ * Assign a value to a positional or named variable in prepared query. If using
+ * positional variables you need to start with index one, if using named params then
+ * just use the name in any order.
+ *
+ * You can pass PDO compatible constants for binding values with a type or optionally
+ * any type name registered in the Type class. Any value will be converted to the valid type
+ * representation if needed.
+ *
+ * It is not allowed to combine positional and named variables in the same statement
+ *
+ * ### Examples:
+ *
+ * ```
+ * $statement->bindValue(1, 'a title');
+ * $statement->bindValue(2, 5, PDO::INT);
+ * $statement->bindValue('active', true, 'boolean');
+ * $statement->bindValue(5, new \DateTime(), 'date');
+ * ```
+ *
+ * @param string|int $column name or param position to be bound
+ * @param mixed $value The value to bind to variable in query
+ * @param string|int|null $type PDO type or name of configured Type class
+ * @return void
+ */
+ public function bindValue($column, $value, $type = 'string'): void
+ {
+ if ($type === null) {
+ $type = 'string';
+ }
+ if (!is_int($type)) {
+ [$value, $type] = $this->cast($value, $type);
+ }
+ $this->_statement->bindValue($column, $value, $type);
+ }
+
+ /**
+ * Returns the next row for the result set after executing this statement.
+ * Rows can be fetched to contain columns as names or positions. If no
+ * rows are left in result set, this method will return false
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title']
+ * ```
+ *
+ * @param string|int $type 'num' for positional columns, assoc for named columns
+ * @return mixed Result array containing columns and values or false if no results
+ * are left
+ */
+ public function fetch($type = parent::FETCH_TYPE_NUM)
+ {
+ if ($type === static::FETCH_TYPE_NUM) {
+ return $this->_statement->fetch(PDO::FETCH_NUM);
+ }
+ if ($type === static::FETCH_TYPE_ASSOC) {
+ return $this->_statement->fetch(PDO::FETCH_ASSOC);
+ }
+ if ($type === static::FETCH_TYPE_OBJ) {
+ return $this->_statement->fetch(PDO::FETCH_OBJ);
+ }
+
+ if (!is_int($type)) {
+ throw new CakeException(sprintf(
+ 'Fetch type for PDOStatement must be an integer, found `%s` instead',
+ getTypeName($type)
+ ));
+ }
+
+ return $this->_statement->fetch($type);
+ }
+
+ /**
+ * Returns an array with all rows resulting from executing this statement
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']]
+ * ```
+ *
+ * @param string|int $type num for fetching columns as positional keys or assoc for column names as keys
+ * @return array|false list of all results from database for this statement, false on failure
+ * @psalm-assert string $type
+ */
+ public function fetchAll($type = parent::FETCH_TYPE_NUM)
+ {
+ if ($type === static::FETCH_TYPE_NUM) {
+ return $this->_statement->fetchAll(PDO::FETCH_NUM);
+ }
+ if ($type === static::FETCH_TYPE_ASSOC) {
+ return $this->_statement->fetchAll(PDO::FETCH_ASSOC);
+ }
+ if ($type === static::FETCH_TYPE_OBJ) {
+ return $this->_statement->fetchAll(PDO::FETCH_OBJ);
+ }
+
+ if (!is_int($type)) {
+ throw new CakeException(sprintf(
+ 'Fetch type for PDOStatement must be an integer, found `%s` instead',
+ getTypeName($type)
+ ));
+ }
+
+ return $this->_statement->fetchAll($type);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/SqliteStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/SqliteStatement.php
new file mode 100644
index 000000000..cc965a812
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/SqliteStatement.php
@@ -0,0 +1,70 @@
+_statement instanceof BufferedStatement) {
+ $this->_statement = $this->_statement->getInnerStatement();
+ }
+
+ if ($this->_bufferResults) {
+ $this->_statement = new BufferedStatement($this->_statement, $this->_driver);
+ }
+
+ return $this->_statement->execute($params);
+ }
+
+ /**
+ * Returns the number of rows returned of affected by last execution
+ *
+ * @return int
+ */
+ public function rowCount(): int
+ {
+ /** @psalm-suppress NoInterfaceProperties */
+ if (
+ $this->_statement->queryString &&
+ preg_match('/^(?:DELETE|UPDATE|INSERT)/i', $this->_statement->queryString)
+ ) {
+ $changes = $this->_driver->prepare('SELECT CHANGES()');
+ $changes->execute();
+ $row = $changes->fetch();
+ $changes->closeCursor();
+
+ if (!$row) {
+ return 0;
+ }
+
+ return (int)$row[0];
+ }
+
+ return parent::rowCount();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/SqlserverStatement.php b/app/vendor/cakephp/cakephp/src/Database/Statement/SqlserverStatement.php
new file mode 100644
index 000000000..efd3b6a2c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/SqlserverStatement.php
@@ -0,0 +1,54 @@
+cast($value, $type);
+ }
+ if ($type === PDO::PARAM_LOB) {
+ /** @psalm-suppress UndefinedConstant */
+ $this->_statement->bindParam($column, $value, $type, 0, PDO::SQLSRV_ENCODING_BINARY);
+ } else {
+ $this->_statement->bindValue($column, $value, $type);
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php b/app/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php
new file mode 100644
index 000000000..b7eb3c36a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php
@@ -0,0 +1,363 @@
+_statement = $statement;
+ $this->_driver = $driver;
+ }
+
+ /**
+ * Magic getter to return $queryString as read-only.
+ *
+ * @param string $property internal property to get
+ * @return mixed
+ */
+ public function __get(string $property)
+ {
+ if ($property === 'queryString') {
+ /** @psalm-suppress NoInterfaceProperties */
+ return $this->_statement->queryString;
+ }
+ }
+
+ /**
+ * Assign a value to a positional or named variable in prepared query. If using
+ * positional variables you need to start with index one, if using named params then
+ * just use the name in any order.
+ *
+ * It is not allowed to combine positional and named variables in the same statement.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $statement->bindValue(1, 'a title');
+ * $statement->bindValue('active', true, 'boolean');
+ * $statement->bindValue(5, new \DateTime(), 'date');
+ * ```
+ *
+ * @param string|int $column name or param position to be bound
+ * @param mixed $value The value to bind to variable in query
+ * @param string|int|null $type name of configured Type class
+ * @return void
+ */
+ public function bindValue($column, $value, $type = 'string'): void
+ {
+ $this->_statement->bindValue($column, $value, $type);
+ }
+
+ /**
+ * Closes a cursor in the database, freeing up any resources and memory
+ * allocated to it. In most cases you don't need to call this method, as it is
+ * automatically called after fetching all results from the result set.
+ *
+ * @return void
+ */
+ public function closeCursor(): void
+ {
+ $this->_statement->closeCursor();
+ }
+
+ /**
+ * Returns the number of columns this statement's results will contain.
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * echo $statement->columnCount(); // outputs 2
+ * ```
+ *
+ * @return int
+ */
+ public function columnCount(): int
+ {
+ return $this->_statement->columnCount();
+ }
+
+ /**
+ * Returns the error code for the last error that occurred when executing this statement.
+ *
+ * @return int|string
+ */
+ public function errorCode()
+ {
+ return $this->_statement->errorCode();
+ }
+
+ /**
+ * Returns the error information for the last error that occurred when executing
+ * this statement.
+ *
+ * @return array
+ */
+ public function errorInfo(): array
+ {
+ return $this->_statement->errorInfo();
+ }
+
+ /**
+ * Executes the statement by sending the SQL query to the database. It can optionally
+ * take an array or arguments to be bound to the query variables. Please note
+ * that binding parameters from this method will not perform any custom type conversion
+ * as it would normally happen when calling `bindValue`.
+ *
+ * @param array|null $params list of values to be bound to query
+ * @return bool true on success, false otherwise
+ */
+ public function execute(?array $params = null): bool
+ {
+ $this->_hasExecuted = true;
+
+ return $this->_statement->execute($params);
+ }
+
+ /**
+ * Returns the next row for the result set after executing this statement.
+ * Rows can be fetched to contain columns as names or positions. If no
+ * rows are left in result set, this method will return false.
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title']
+ * ```
+ *
+ * @param string|int $type 'num' for positional columns, assoc for named columns
+ * @return mixed Result array containing columns and values or false if no results
+ * are left
+ */
+ public function fetch($type = self::FETCH_TYPE_NUM)
+ {
+ return $this->_statement->fetch($type);
+ }
+
+ /**
+ * Returns the next row in a result set as an associative array. Calling this function is the same as calling
+ * $statement->fetch(StatementDecorator::FETCH_TYPE_ASSOC). If no results are found false is returned.
+ *
+ * @return array Result array containing columns and values an an associative array or an empty array if no results
+ */
+ public function fetchAssoc(): array
+ {
+ $result = $this->fetch(static::FETCH_TYPE_ASSOC);
+
+ return $result ?: [];
+ }
+
+ /**
+ * Returns the value of the result at position.
+ *
+ * @param int $position The numeric position of the column to retrieve in the result
+ * @return mixed Returns the specific value of the column designated at $position
+ */
+ public function fetchColumn(int $position)
+ {
+ $result = $this->fetch(static::FETCH_TYPE_NUM);
+ if ($result && isset($result[$position])) {
+ return $result[$position];
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an array with all rows resulting from executing this statement.
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']]
+ * ```
+ *
+ * @param string|int $type num for fetching columns as positional keys or assoc for column names as keys
+ * @return array|false List of all results from database for this statement. False on failure.
+ */
+ public function fetchAll($type = self::FETCH_TYPE_NUM)
+ {
+ return $this->_statement->fetchAll($type);
+ }
+
+ /**
+ * Returns the number of rows affected by this SQL statement.
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->rowCount()); // will show 1
+ * ```
+ *
+ * @return int
+ */
+ public function rowCount(): int
+ {
+ return $this->_statement->rowCount();
+ }
+
+ /**
+ * Statements are iterable as arrays, this method will return
+ * the iterator object for traversing all items in the result.
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * foreach ($statement as $row) {
+ * //do stuff
+ * }
+ * ```
+ *
+ * @return \Cake\Database\StatementInterface
+ * @psalm-suppress ImplementedReturnTypeMismatch
+ */
+ public function getIterator()
+ {
+ if (!$this->_hasExecuted) {
+ $this->execute();
+ }
+
+ return $this->_statement;
+ }
+
+ /**
+ * Statements can be passed as argument for count() to return the number
+ * for affected rows from last execution.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return $this->rowCount();
+ }
+
+ /**
+ * Binds a set of values to statement object with corresponding type.
+ *
+ * @param array $params list of values to be bound
+ * @param array $types list of types to be used, keys should match those in $params
+ * @return void
+ */
+ public function bind(array $params, array $types): void
+ {
+ if (empty($params)) {
+ return;
+ }
+
+ $anonymousParams = is_int(key($params)) ? true : false;
+ $offset = 1;
+ foreach ($params as $index => $value) {
+ $type = null;
+ if (isset($types[$index])) {
+ $type = $types[$index];
+ }
+ if ($anonymousParams) {
+ /** @psalm-suppress InvalidOperand */
+ $index += $offset;
+ }
+ /** @psalm-suppress InvalidScalarArgument */
+ $this->bindValue($index, $value, $type);
+ }
+ }
+
+ /**
+ * Returns the latest primary inserted using this statement.
+ *
+ * @param string|null $table table name or sequence to get last insert value from
+ * @param string|null $column the name of the column representing the primary key
+ * @return string|int
+ */
+ public function lastInsertId(?string $table = null, ?string $column = null)
+ {
+ if ($column && $this->columnCount()) {
+ $row = $this->fetch(static::FETCH_TYPE_ASSOC);
+
+ if ($row && isset($row[$column])) {
+ return $row[$column];
+ }
+ }
+
+ return $this->_driver->lastInsertId($table, $column);
+ }
+
+ /**
+ * Returns the statement object that was decorated by this class.
+ *
+ * @return \Cake\Database\StatementInterface
+ */
+ public function getInnerStatement()
+ {
+ return $this->_statement;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/StatementInterface.php b/app/vendor/cakephp/cakephp/src/Database/StatementInterface.php
new file mode 100644
index 000000000..0c0dc5f86
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/StatementInterface.php
@@ -0,0 +1,203 @@
+bindValue(1, 'a title');
+ * $statement->bindValue('active', true, 'boolean');
+ * $statement->bindValue(5, new \DateTime(), 'date');
+ * ```
+ *
+ * @param string|int $column name or param position to be bound
+ * @param mixed $value The value to bind to variable in query
+ * @param string|int|null $type name of configured Type class, or PDO type constant.
+ * @return void
+ */
+ public function bindValue($column, $value, $type = 'string'): void;
+
+ /**
+ * Closes a cursor in the database, freeing up any resources and memory
+ * allocated to it. In most cases you don't need to call this method, as it is
+ * automatically called after fetching all results from the result set.
+ *
+ * @return void
+ */
+ public function closeCursor(): void;
+
+ /**
+ * Returns the number of columns this statement's results will contain
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * echo $statement->columnCount(); // outputs 2
+ * ```
+ *
+ * @return int
+ */
+ public function columnCount(): int;
+
+ /**
+ * Returns the error code for the last error that occurred when executing this statement
+ *
+ * @return int|string
+ */
+ public function errorCode();
+
+ /**
+ * Returns the error information for the last error that occurred when executing
+ * this statement
+ *
+ * @return array
+ */
+ public function errorInfo(): array;
+
+ /**
+ * Executes the statement by sending the SQL query to the database. It can optionally
+ * take an array or arguments to be bound to the query variables. Please note
+ * that binding parameters from this method will not perform any custom type conversion
+ * as it would normally happen when calling `bindValue`
+ *
+ * @param array|null $params list of values to be bound to query
+ * @return bool true on success, false otherwise
+ */
+ public function execute(?array $params = null): bool;
+
+ /**
+ * Returns the next row for the result set after executing this statement.
+ * Rows can be fetched to contain columns as names or positions. If no
+ * rows are left in result set, this method will return false
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->fetch('assoc')); // will show ['id' => 1, 'title' => 'a title']
+ * ```
+ *
+ * @param string|int $type 'num' for positional columns, assoc for named columns, or PDO fetch mode constants.
+ * @return mixed Result array containing columns and values or false if no results
+ * are left
+ */
+ public function fetch($type = 'num');
+
+ /**
+ * Returns an array with all rows resulting from executing this statement
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->fetchAll('assoc')); // will show [0 => ['id' => 1, 'title' => 'a title']]
+ * ```
+ *
+ * @param string|int $type num for fetching columns as positional keys or assoc for column names as keys
+ * @return array|false list of all results from database for this statement or false on failure.
+ */
+ public function fetchAll($type = 'num');
+
+ /**
+ * Returns the value of the result at position.
+ *
+ * @param int $position The numeric position of the column to retrieve in the result
+ * @return mixed Returns the specific value of the column designated at $position
+ */
+ public function fetchColumn(int $position);
+
+ /**
+ * Returns the number of rows affected by this SQL statement
+ *
+ * ### Example:
+ *
+ * ```
+ * $statement = $connection->prepare('SELECT id, title from articles');
+ * $statement->execute();
+ * print_r($statement->rowCount()); // will show 1
+ * ```
+ *
+ * @return int
+ */
+ public function rowCount(): int;
+
+ /**
+ * Statements can be passed as argument for count()
+ * to return the number for affected rows from last execution
+ *
+ * @return int
+ */
+ public function count(): int;
+
+ /**
+ * Binds a set of values to statement object with corresponding type
+ *
+ * @param array $params list of values to be bound
+ * @param array $types list of types to be used, keys should match those in $params
+ * @return void
+ */
+ public function bind(array $params, array $types): void;
+
+ /**
+ * Returns the latest primary inserted using this statement
+ *
+ * @param string|null $table table name or sequence to get last insert value from
+ * @param string|null $column the name of the column representing the primary key
+ * @return string|int
+ */
+ public function lastInsertId(?string $table = null, ?string $column = null);
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type.php b/app/vendor/cakephp/cakephp/src/Database/Type.php
new file mode 100644
index 000000000..466b8d597
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type.php
@@ -0,0 +1,5 @@
+_name = $name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getName(): ?string
+ {
+ return $this->_name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBaseType(): ?string
+ {
+ return $this->_name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toStatement($value, DriverInterface $driver)
+ {
+ if ($value === null) {
+ return PDO::PARAM_NULL;
+ }
+
+ return PDO::PARAM_STR;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function newId()
+ {
+ return null;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/BatchCastingInterface.php b/app/vendor/cakephp/cakephp/src/Database/Type/BatchCastingInterface.php
new file mode 100644
index 000000000..04b74c722
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/BatchCastingInterface.php
@@ -0,0 +1,37 @@
+convertStringToBinaryUuid($value);
+ }
+
+ /**
+ * Generate a new binary UUID
+ *
+ * @return string A new primary key value.
+ */
+ public function newId(): string
+ {
+ return Text::uuid();
+ }
+
+ /**
+ * Convert binary uuid into resource handles
+ *
+ * @param mixed $value The value to convert.
+ * @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
+ * @return resource|string|null
+ * @throws \Cake\Core\Exception\CakeException
+ */
+ public function toPHP($value, DriverInterface $driver)
+ {
+ if ($value === null) {
+ return null;
+ }
+ if (is_string($value)) {
+ return $this->convertBinaryUuidToString($value);
+ }
+ if (is_resource($value)) {
+ return $value;
+ }
+
+ throw new CakeException(sprintf('Unable to convert %s into binary uuid.', gettype($value)));
+ }
+
+ /**
+ * Get the correct PDO binding type for Binary data.
+ *
+ * @param mixed $value The value being bound.
+ * @param \Cake\Database\DriverInterface $driver The driver.
+ * @return int
+ */
+ public function toStatement($value, DriverInterface $driver): int
+ {
+ return PDO::PARAM_LOB;
+ }
+
+ /**
+ * Marshals flat data into PHP objects.
+ *
+ * Most useful for converting request data into PHP objects
+ * that make sense for the rest of the ORM/Database layers.
+ *
+ * @param mixed $value The value to convert.
+ * @return mixed Converted value.
+ */
+ public function marshal($value)
+ {
+ return $value;
+ }
+
+ /**
+ * Converts a binary uuid to a string representation
+ *
+ * @param mixed $binary The value to convert.
+ * @return string Converted value.
+ */
+ protected function convertBinaryUuidToString($binary): string
+ {
+ $string = unpack('H*', $binary);
+
+ $string = preg_replace(
+ '/([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})/',
+ '$1-$2-$3-$4-$5',
+ $string
+ );
+
+ return $string[1];
+ }
+
+ /**
+ * Converts a string UUID (36 or 32 char) to a binary representation.
+ *
+ * @param string $string The value to convert.
+ * @return string Converted value.
+ */
+ protected function convertStringToBinaryUuid($string): string
+ {
+ $string = str_replace('-', '', $string);
+
+ return pack('H*', $string);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/BoolType.php b/app/vendor/cakephp/cakephp/src/Database/Type/BoolType.php
new file mode 100644
index 000000000..eeb1c8854
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/BoolType.php
@@ -0,0 +1,135 @@
+|class-string<\DateTimeImmutable>
+ */
+ protected $_className;
+
+ /**
+ * Database time zone.
+ *
+ * @var \DateTimeZone|null
+ */
+ protected $dbTimezone;
+
+ /**
+ * Default time zone.
+ *
+ * @var \DateTimeZone
+ */
+ protected $defaultTimezone;
+
+ /**
+ * Whether database time zone is kept when converting
+ *
+ * @var bool
+ */
+ protected $keepDatabaseTimezone = false;
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string|null $name The name identifying this type
+ */
+ public function __construct(?string $name = null)
+ {
+ parent::__construct($name);
+
+ $this->defaultTimezone = new DateTimeZone(date_default_timezone_get());
+ $this->useImmutable();
+ }
+
+ /**
+ * Convert DateTime instance into strings.
+ *
+ * @param mixed $value The value to convert.
+ * @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
+ * @return string|null
+ */
+ public function toDatabase($value, DriverInterface $driver): ?string
+ {
+ if ($value === null || is_string($value)) {
+ return $value;
+ }
+ if (is_int($value)) {
+ $class = $this->_className;
+ $value = new $class('@' . $value);
+ }
+
+ if (
+ $this->dbTimezone !== null
+ && $this->dbTimezone->getName() !== $value->getTimezone()->getName()
+ ) {
+ if (!$value instanceof DateTimeImmutable) {
+ $value = clone $value;
+ }
+ $value = $value->setTimezone($this->dbTimezone);
+ }
+
+ return $value->format($this->_format);
+ }
+
+ /**
+ * Alias for `setDatabaseTimezone()`.
+ *
+ * @param string|\DateTimeZone|null $timezone Database timezone.
+ * @return $this
+ * @deprecated 4.1.0 Use {@link setDatabaseTimezone()} instead.
+ */
+ public function setTimezone($timezone)
+ {
+ deprecationWarning('DateTimeType::setTimezone() is deprecated. Use setDatabaseTimezone() instead.');
+
+ return $this->setDatabaseTimezone($timezone);
+ }
+
+ /**
+ * Set database timezone.
+ *
+ * This is the time zone used when converting database strings to DateTime
+ * instances and converting DateTime instances to database strings.
+ *
+ * @see DateTimeType::setKeepDatabaseTimezone
+ * @param string|\DateTimeZone|null $timezone Database timezone.
+ * @return $this
+ */
+ public function setDatabaseTimezone($timezone)
+ {
+ if (is_string($timezone)) {
+ $timezone = new DateTimeZone($timezone);
+ }
+ $this->dbTimezone = $timezone;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param mixed $value Value to be converted to PHP equivalent
+ * @param \Cake\Database\DriverInterface $driver Object from which database preferences and configuration will be extracted
+ * @return \DateTimeInterface|null
+ */
+ public function toPHP($value, DriverInterface $driver)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ $class = $this->_className;
+ if (is_int($value)) {
+ $instance = new $class('@' . $value);
+ } else {
+ if (strpos($value, '0000-00-00') === 0) {
+ return null;
+ }
+ $instance = new $class($value, $this->dbTimezone);
+ }
+
+ if (
+ !$this->keepDatabaseTimezone &&
+ $instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
+ ) {
+ $instance = $instance->setTimezone($this->defaultTimezone);
+ }
+
+ if ($this->setToDateStart) {
+ $instance = $instance->setTime(0, 0, 0);
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Set whether DateTime object created from database string is converted
+ * to default time zone.
+ *
+ * If your database date times are in a specific time zone that you want
+ * to keep in the DateTime instance then set this to true.
+ *
+ * When false, datetime timezones are converted to default time zone.
+ * This is default behavior.
+ *
+ * @param bool $keep If true, database time zone is kept when converting
+ * to DateTime instances.
+ * @return $this
+ */
+ public function setKeepDatabaseTimezone(bool $keep)
+ {
+ $this->keepDatabaseTimezone = $keep;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function manyToPHP(array $values, array $fields, DriverInterface $driver): array
+ {
+ foreach ($fields as $field) {
+ if (!isset($values[$field])) {
+ continue;
+ }
+
+ $value = $values[$field];
+ if (strpos($value, '0000-00-00') === 0) {
+ $values[$field] = null;
+ continue;
+ }
+
+ $class = $this->_className;
+ if (is_int($value)) {
+ $instance = new $class('@' . $value);
+ } else {
+ $instance = new $class($value, $this->dbTimezone);
+ }
+
+ if (
+ !$this->keepDatabaseTimezone &&
+ $instance->getTimezone()->getName() !== $this->defaultTimezone->getName()
+ ) {
+ $instance = $instance->setTimezone($this->defaultTimezone);
+ }
+
+ if ($this->setToDateStart) {
+ $instance = $instance->setTime(0, 0, 0);
+ }
+
+ $values[$field] = $instance;
+ }
+
+ return $values;
+ }
+
+ /**
+ * Convert request data into a datetime object.
+ *
+ * @param mixed $value Request data
+ * @return \DateTimeInterface|null
+ */
+ public function marshal($value): ?DateTimeInterface
+ {
+ if ($value instanceof DateTimeInterface) {
+ return $value;
+ }
+
+ /** @var class-string<\DatetimeInterface> $class */
+ $class = $this->_className;
+ try {
+ if ($value === '' || $value === null || is_bool($value)) {
+ return null;
+ }
+ $isString = is_string($value);
+ if (ctype_digit($value)) {
+ return new $class('@' . $value);
+ } elseif ($isString && $this->_useLocaleMarshal) {
+ return $this->_parseLocaleValue($value);
+ } elseif ($isString) {
+ return $this->_parseValue($value);
+ }
+ } catch (Exception $e) {
+ return null;
+ }
+
+ if (is_array($value) && implode('', $value) === '') {
+ return null;
+ }
+ $value += ['hour' => 0, 'minute' => 0, 'second' => 0, 'microsecond' => 0];
+
+ $format = '';
+ if (
+ isset($value['year'], $value['month'], $value['day']) &&
+ (
+ is_numeric($value['year']) &&
+ is_numeric($value['month']) &&
+ is_numeric($value['day'])
+ )
+ ) {
+ $format .= sprintf('%d-%02d-%02d', $value['year'], $value['month'], $value['day']);
+ }
+
+ if (isset($value['meridian']) && (int)$value['hour'] === 12) {
+ $value['hour'] = 0;
+ }
+ if (isset($value['meridian'])) {
+ $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
+ }
+ $format .= sprintf(
+ '%s%02d:%02d:%02d.%06d',
+ empty($format) ? '' : ' ',
+ $value['hour'],
+ $value['minute'],
+ $value['second'],
+ $value['microsecond']
+ );
+ $tz = $value['timezone'] ?? null;
+
+ return new $class($format, $tz);
+ }
+
+ /**
+ * Sets whether or not to parse strings passed to `marshal()` using
+ * the locale-aware format set by `setLocaleFormat()`.
+ *
+ * @param bool $enable Whether or not to enable
+ * @return $this
+ */
+ public function useLocaleParser(bool $enable = true)
+ {
+ if ($enable === false) {
+ $this->_useLocaleMarshal = $enable;
+
+ return $this;
+ }
+ if (is_subclass_of($this->_className, I18nDateTimeInterface::class)) {
+ $this->_useLocaleMarshal = $enable;
+
+ return $this;
+ }
+ throw new RuntimeException(
+ sprintf('Cannot use locale parsing with the %s class', $this->_className)
+ );
+ }
+
+ /**
+ * Sets the locale-aware format used by `marshal()` when parsing strings.
+ *
+ * See `Cake\I18n\Time::parseDateTime()` for accepted formats.
+ *
+ * @param string|array $format The locale-aware format
+ * @see \Cake\I18n\Time::parseDateTime()
+ * @return $this
+ */
+ public function setLocaleFormat($format)
+ {
+ $this->_localeMarshalFormat = $format;
+
+ return $this;
+ }
+
+ /**
+ * Change the preferred class name to the FrozenTime implementation.
+ *
+ * @return $this
+ */
+ public function useImmutable()
+ {
+ $this->_setClassName(FrozenTime::class, DateTimeImmutable::class);
+
+ return $this;
+ }
+
+ /**
+ * Set the classname to use when building objects.
+ *
+ * @param string $class The classname to use.
+ * @param string $fallback The classname to use when the preferred class does not exist.
+ * @return void
+ * @psalm-param class-string<\DateTime>|class-string<\DateTimeImmutable> $class
+ * @psalm-param class-string<\DateTime>|class-string<\DateTimeImmutable> $fallback
+ */
+ protected function _setClassName(string $class, string $fallback): void
+ {
+ if (!class_exists($class)) {
+ $class = $fallback;
+ }
+ $this->_className = $class;
+ }
+
+ /**
+ * Get the classname used for building objects.
+ *
+ * @return string
+ * @psalm-return class-string<\DateTime>|class-string<\DateTimeImmutable>
+ */
+ public function getDateTimeClassName(): string
+ {
+ return $this->_className;
+ }
+
+ /**
+ * Change the preferred class name to the mutable Time implementation.
+ *
+ * @return $this
+ */
+ public function useMutable()
+ {
+ $this->_setClassName(Time::class, DateTime::class);
+
+ return $this;
+ }
+
+ /**
+ * Converts a string into a DateTime object after parsing it using the locale
+ * aware parser with the format set by `setLocaleFormat()`.
+ *
+ * @param string $value The value to parse and convert to an object.
+ * @return \Cake\I18n\I18nDateTimeInterface|null
+ */
+ protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
+ {
+ /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */
+ $class = $this->_className;
+
+ return $class::parseDateTime($value, $this->_localeMarshalFormat);
+ }
+
+ /**
+ * Converts a string into a DateTime object after parsing it using the
+ * formats in `_marshalFormats`.
+ *
+ * @param string $value The value to parse and convert to an object.
+ * @return \DateTimeInterface|null
+ */
+ protected function _parseValue(string $value): ?DateTimeInterface
+ {
+ $class = $this->_className;
+
+ foreach ($this->_marshalFormats as $format) {
+ try {
+ $dateTime = $class::createFromFormat($format, $value);
+ // Check for false in case DateTime is used directly
+ if ($dateTime !== false) {
+ return $dateTime;
+ }
+ } catch (InvalidArgumentException $e) {
+ // Chronos wraps DateTime::createFromFormat and throws
+ // exception if parse fails.
+ continue;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Casts given value to Statement equivalent
+ *
+ * @param mixed $value value to be converted to PDO statement
+ * @param \Cake\Database\DriverInterface $driver object from which database preferences and configuration will be extracted
+ * @return mixed
+ */
+ public function toStatement($value, DriverInterface $driver)
+ {
+ return PDO::PARAM_STR;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/DateType.php b/app/vendor/cakephp/cakephp/src/Database/Type/DateType.php
new file mode 100644
index 000000000..773b30bd6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/DateType.php
@@ -0,0 +1,103 @@
+_setClassName(FrozenDate::class, DateTimeImmutable::class);
+
+ return $this;
+ }
+
+ /**
+ * Change the preferred class name to the mutable Date implementation.
+ *
+ * @return $this
+ */
+ public function useMutable()
+ {
+ $this->_setClassName(Date::class, DateTime::class);
+
+ return $this;
+ }
+
+ /**
+ * Convert request data into a datetime object.
+ *
+ * @param mixed $value Request data
+ * @return \DateTimeInterface|null
+ */
+ public function marshal($value): ?DateTimeInterface
+ {
+ $date = parent::marshal($value);
+ if ($date && !$date instanceof I18nDateTimeInterface) {
+ // Clear time manually when I18n types aren't available and raw DateTime used
+ /** @psalm-var \DateTime|\DateTimeImmutable $date */
+ $date->setTime(0, 0, 0);
+ }
+
+ return $date;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _parseLocaleValue(string $value): ?I18nDateTimeInterface
+ {
+ /** @psalm-var class-string<\Cake\I18n\I18nDateTimeInterface> $class */
+ $class = $this->_className;
+
+ return $class::parseDate($value, $this->_localeMarshalFormat);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/DecimalType.php b/app/vendor/cakephp/cakephp/src/Database/Type/DecimalType.php
new file mode 100644
index 000000000..9359ca1d3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/DecimalType.php
@@ -0,0 +1,189 @@
+_useLocaleParser) {
+ return $this->_parseValue($value);
+ }
+ if (is_numeric($value)) {
+ return (string)$value;
+ }
+ if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) {
+ return $value;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets whether or not to parse numbers passed to the marshal() function
+ * by using a locale aware parser.
+ *
+ * @param bool $enable Whether or not to enable
+ * @return $this
+ * @throws \RuntimeException
+ */
+ public function useLocaleParser(bool $enable = true)
+ {
+ if ($enable === false) {
+ $this->_useLocaleParser = $enable;
+
+ return $this;
+ }
+ if (
+ static::$numberClass === Number::class ||
+ is_subclass_of(static::$numberClass, Number::class)
+ ) {
+ $this->_useLocaleParser = $enable;
+
+ return $this;
+ }
+ throw new RuntimeException(
+ sprintf('Cannot use locale parsing with the %s class', static::$numberClass)
+ );
+ }
+
+ /**
+ * Converts localized string into a decimal string after parsing it using
+ * the locale aware parser.
+ *
+ * @param string $value The value to parse and convert to an float.
+ * @return string
+ */
+ protected function _parseValue(string $value): string
+ {
+ /** @var \Cake\I18n\Number $class */
+ $class = static::$numberClass;
+
+ return (string)$class::parseFloat($value);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeCasterTrait.php b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeCasterTrait.php
new file mode 100644
index 000000000..597c18ad6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeCasterTrait.php
@@ -0,0 +1,80 @@
+toExpression($value);
+ }
+
+ /**
+ * Returns an array with the types that require values to
+ * be casted to expressions, out of the list of type names
+ * passed as parameter.
+ *
+ * @param array $types List of type names
+ * @return array
+ */
+ protected function _requiresToExpressionCasting(array $types): array
+ {
+ $result = [];
+ $types = array_filter($types);
+ foreach ($types as $k => $type) {
+ $object = TypeFactory::build($type);
+ if ($object instanceof ExpressionTypeInterface) {
+ $result[$k] = $object;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeInterface.php b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeInterface.php
new file mode 100644
index 000000000..7615cf9e0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/ExpressionTypeInterface.php
@@ -0,0 +1,36 @@
+_useLocaleParser) {
+ return $this->_parseValue($value);
+ }
+ if (is_numeric($value)) {
+ return (float)$value;
+ }
+ if (is_string($value) && preg_match('/^[0-9,. ]+$/', $value)) {
+ return $value;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets whether or not to parse numbers passed to the marshal() function
+ * by using a locale aware parser.
+ *
+ * @param bool $enable Whether or not to enable
+ * @return $this
+ */
+ public function useLocaleParser(bool $enable = true)
+ {
+ if ($enable === false) {
+ $this->_useLocaleParser = $enable;
+
+ return $this;
+ }
+ if (
+ static::$numberClass === Number::class ||
+ is_subclass_of(static::$numberClass, Number::class)
+ ) {
+ $this->_useLocaleParser = $enable;
+
+ return $this;
+ }
+ throw new RuntimeException(
+ sprintf('Cannot use locale parsing with the %s class', static::$numberClass)
+ );
+ }
+
+ /**
+ * Converts a string into a float point after parsing it using the locale
+ * aware parser.
+ *
+ * @param string $value The value to parse and convert to an float.
+ * @return float
+ */
+ protected function _parseValue(string $value): float
+ {
+ $class = static::$numberClass;
+
+ return $class::parseFloat($value);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/IntegerType.php b/app/vendor/cakephp/cakephp/src/Database/Type/IntegerType.php
new file mode 100644
index 000000000..8352e4f11
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/IntegerType.php
@@ -0,0 +1,128 @@
+checkNumeric($value);
+
+ return (int)$value;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param mixed $value The value to convert.
+ * @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
+ * @return int|null
+ */
+ public function toPHP($value, DriverInterface $driver): ?int
+ {
+ if ($value === null) {
+ return $value;
+ }
+
+ return (int)$value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function manyToPHP(array $values, array $fields, DriverInterface $driver): array
+ {
+ foreach ($fields as $field) {
+ if (!isset($values[$field])) {
+ continue;
+ }
+
+ $this->checkNumeric($values[$field]);
+
+ $values[$field] = (int)$values[$field];
+ }
+
+ return $values;
+ }
+
+ /**
+ * Get the correct PDO binding type for integer data.
+ *
+ * @param mixed $value The value being bound.
+ * @param \Cake\Database\DriverInterface $driver The driver.
+ * @return int
+ */
+ public function toStatement($value, DriverInterface $driver): int
+ {
+ return PDO::PARAM_INT;
+ }
+
+ /**
+ * Marshals request data into PHP floats.
+ *
+ * @param mixed $value The value to convert.
+ * @return int|null Converted value.
+ */
+ public function marshal($value): ?int
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+ if (is_numeric($value)) {
+ return (int)$value;
+ }
+
+ return null;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/JsonType.php b/app/vendor/cakephp/cakephp/src/Database/Type/JsonType.php
new file mode 100644
index 000000000..bd9544926
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/JsonType.php
@@ -0,0 +1,105 @@
+__toString();
+ }
+
+ if (is_scalar($value)) {
+ return (string)$value;
+ }
+
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot convert value of type `%s` to string',
+ getTypeName($value)
+ ));
+ }
+
+ /**
+ * Convert string values to PHP strings.
+ *
+ * @param mixed $value The value to convert.
+ * @param \Cake\Database\DriverInterface $driver The driver instance to convert with.
+ * @return string|null
+ */
+ public function toPHP($value, DriverInterface $driver): ?string
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ return (string)$value;
+ }
+
+ /**
+ * Get the correct PDO binding type for string data.
+ *
+ * @param mixed $value The value being bound.
+ * @param \Cake\Database\DriverInterface $driver The driver.
+ * @return int
+ */
+ public function toStatement($value, DriverInterface $driver): int
+ {
+ return PDO::PARAM_STR;
+ }
+
+ /**
+ * Marshals request data into PHP strings.
+ *
+ * @param mixed $value The value to convert.
+ * @return string|null Converted value.
+ */
+ public function marshal($value): ?string
+ {
+ if ($value === null || is_array($value)) {
+ return null;
+ }
+
+ return (string)$value;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return bool False as database results are returned already as strings
+ */
+ public function requiresToPhpCast(): bool
+ {
+ return false;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/TimeType.php b/app/vendor/cakephp/cakephp/src/Database/Type/TimeType.php
new file mode 100644
index 000000000..4c021e214
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/TimeType.php
@@ -0,0 +1,52 @@
+ $class */
+ $class = $this->_className;
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ return $class::parseTime($value, $this->_localeMarshalFormat);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/Type/UuidType.php b/app/vendor/cakephp/cakephp/src/Database/Type/UuidType.php
new file mode 100644
index 000000000..ce7f754fb
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/Type/UuidType.php
@@ -0,0 +1,67 @@
+toDatabase($value, $this->_driver);
+ $type = $type->toStatement($value, $this->_driver);
+ }
+
+ return [$value, $type];
+ }
+
+ /**
+ * Matches columns to corresponding types
+ *
+ * Both $columns and $types should either be numeric based or string key based at
+ * the same time.
+ *
+ * @param array $columns list or associative array of columns and parameters to be bound with types
+ * @param array $types list or associative array of types
+ * @return array
+ */
+ public function matchTypes(array $columns, array $types): array
+ {
+ if (!is_int(key($types))) {
+ $positions = array_intersect_key(array_flip($columns), $types);
+ $types = array_intersect_key($types, $positions);
+ $types = array_combine($positions, $types);
+ }
+
+ return $types;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/TypeFactory.php b/app/vendor/cakephp/cakephp/src/Database/TypeFactory.php
new file mode 100644
index 000000000..361dfc9a7
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/TypeFactory.php
@@ -0,0 +1,164 @@
+
+ * @psalm-var array>
+ */
+ protected static $_types = [
+ 'tinyinteger' => Type\IntegerType::class,
+ 'smallinteger' => Type\IntegerType::class,
+ 'integer' => Type\IntegerType::class,
+ 'biginteger' => Type\IntegerType::class,
+ 'binary' => Type\BinaryType::class,
+ 'binaryuuid' => Type\BinaryUuidType::class,
+ 'boolean' => Type\BoolType::class,
+ 'date' => Type\DateType::class,
+ 'datetime' => Type\DateTimeType::class,
+ 'datetimefractional' => Type\DateTimeFractionalType::class,
+ 'decimal' => Type\DecimalType::class,
+ 'float' => Type\FloatType::class,
+ 'json' => Type\JsonType::class,
+ 'string' => Type\StringType::class,
+ 'char' => Type\StringType::class,
+ 'text' => Type\StringType::class,
+ 'time' => Type\TimeType::class,
+ 'timestamp' => Type\DateTimeType::class,
+ 'timestampfractional' => Type\DateTimeFractionalType::class,
+ 'timestamptimezone' => Type\DateTimeTimezoneType::class,
+ 'uuid' => Type\UuidType::class,
+ ];
+
+ /**
+ * Contains a map of type object instances to be reused if needed.
+ *
+ * @var \Cake\Database\TypeInterface[]
+ */
+ protected static $_builtTypes = [];
+
+ /**
+ * Returns a Type object capable of converting a type identified by name.
+ *
+ * @param string $name type identifier
+ * @throws \InvalidArgumentException If type identifier is unknown
+ * @return \Cake\Database\TypeInterface
+ */
+ public static function build(string $name): TypeInterface
+ {
+ if (isset(static::$_builtTypes[$name])) {
+ return static::$_builtTypes[$name];
+ }
+ if (!isset(static::$_types[$name])) {
+ throw new InvalidArgumentException(sprintf('Unknown type "%s"', $name));
+ }
+
+ return static::$_builtTypes[$name] = new static::$_types[$name]($name);
+ }
+
+ /**
+ * Returns an arrays with all the mapped type objects, indexed by name.
+ *
+ * @return \Cake\Database\TypeInterface[]
+ */
+ public static function buildAll(): array
+ {
+ $result = [];
+ foreach (static::$_types as $name => $type) {
+ $result[$name] = static::$_builtTypes[$name] ?? static::build($name);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Set TypeInterface instance capable of converting a type identified by $name
+ *
+ * @param string $name The type identifier you want to set.
+ * @param \Cake\Database\TypeInterface $instance The type instance you want to set.
+ * @return void
+ */
+ public static function set(string $name, TypeInterface $instance): void
+ {
+ static::$_builtTypes[$name] = $instance;
+ static::$_types[$name] = get_class($instance);
+ }
+
+ /**
+ * Registers a new type identifier and maps it to a fully namespaced classname.
+ *
+ * @param string $type Name of type to map.
+ * @param string $className The classname to register.
+ * @return void
+ * @psalm-param class-string<\Cake\Database\TypeInterface> $className
+ */
+ public static function map(string $type, string $className): void
+ {
+ static::$_types[$type] = $className;
+ unset(static::$_builtTypes[$type]);
+ }
+
+ /**
+ * Set type to classname mapping.
+ *
+ * @param string[] $map List of types to be mapped.
+ * @return void
+ * @psalm-param array> $map
+ */
+ public static function setMap(array $map): void
+ {
+ static::$_types = $map;
+ static::$_builtTypes = [];
+ }
+
+ /**
+ * Get mapped class name for given type or map array.
+ *
+ * @param string|null $type Type name to get mapped class for or null to get map array.
+ * @return string[]|string|null Configured class name for given $type or map array.
+ */
+ public static function getMap(?string $type = null)
+ {
+ if ($type === null) {
+ return static::$_types;
+ }
+
+ return static::$_types[$type] ?? null;
+ }
+
+ /**
+ * Clears out all created instances and mapped types classes, useful for testing
+ *
+ * @return void
+ */
+ public static function clear(): void
+ {
+ static::$_types = [];
+ static::$_builtTypes = [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/TypeInterface.php b/app/vendor/cakephp/cakephp/src/Database/TypeInterface.php
new file mode 100644
index 000000000..fca0fdbe8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/TypeInterface.php
@@ -0,0 +1,91 @@
+setDefaults($defaults);
+ }
+
+ /**
+ * Configures a map of fields and associated type.
+ *
+ * These values will be used as the default mapping of types for every function
+ * in this instance that supports a `$types` param.
+ *
+ * This method is useful when you want to avoid repeating type definitions
+ * as setting types overwrites the last set of types.
+ *
+ * ### Example
+ *
+ * ```
+ * $query->setDefaults(['created' => 'datetime', 'is_visible' => 'boolean']);
+ * ```
+ *
+ * This method will replace all the existing default mappings with the ones provided.
+ * To add into the mappings use `addDefaults()`.
+ *
+ * @param string[] $defaults Associative array where keys are field names and values
+ * are the correspondent type.
+ * @return $this
+ */
+ public function setDefaults(array $defaults)
+ {
+ $this->_defaults = $defaults;
+
+ return $this;
+ }
+
+ /**
+ * Returns the currently configured types.
+ *
+ * @return string[]
+ */
+ public function getDefaults(): array
+ {
+ return $this->_defaults;
+ }
+
+ /**
+ * Add additional default types into the type map.
+ *
+ * If a key already exists it will not be overwritten.
+ *
+ * @param string[] $types The additional types to add.
+ * @return void
+ */
+ public function addDefaults(array $types): void
+ {
+ $this->_defaults += $types;
+ }
+
+ /**
+ * Sets a map of fields and their associated types for single-use.
+ *
+ * ### Example
+ *
+ * ```
+ * $query->setTypes(['created' => 'time']);
+ * ```
+ *
+ * This method will replace all the existing type maps with the ones provided.
+ *
+ * @param string[] $types Associative array where keys are field names and values
+ * are the correspondent type.
+ * @return $this
+ */
+ public function setTypes(array $types)
+ {
+ $this->_types = $types;
+
+ return $this;
+ }
+
+ /**
+ * Gets a map of fields and their associated types for single-use.
+ *
+ * @return string[]
+ */
+ public function getTypes(): array
+ {
+ return $this->_types;
+ }
+
+ /**
+ * Returns the type of the given column. If there is no single use type is configured,
+ * the column type will be looked for inside the default mapping. If neither exist,
+ * null will be returned.
+ *
+ * @param string|int $column The type for a given column
+ * @return string|null
+ */
+ public function type($column): ?string
+ {
+ if (isset($this->_types[$column])) {
+ return $this->_types[$column];
+ }
+ if (isset($this->_defaults[$column])) {
+ return $this->_defaults[$column];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns an array of all types mapped types
+ *
+ * @return string[]
+ */
+ public function toArray(): array
+ {
+ return $this->_types + $this->_defaults;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/TypeMapTrait.php b/app/vendor/cakephp/cakephp/src/Database/TypeMapTrait.php
new file mode 100644
index 000000000..e893c5930
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/TypeMapTrait.php
@@ -0,0 +1,89 @@
+_typeMap = is_array($typeMap) ? new TypeMap($typeMap) : $typeMap;
+
+ return $this;
+ }
+
+ /**
+ * Returns the existing type map.
+ *
+ * @return \Cake\Database\TypeMap
+ */
+ public function getTypeMap(): TypeMap
+ {
+ if ($this->_typeMap === null) {
+ $this->_typeMap = new TypeMap();
+ }
+
+ return $this->_typeMap;
+ }
+
+ /**
+ * Overwrite the default type mappings for fields
+ * in the implementing object.
+ *
+ * This method is useful if you need to set type mappings that are shared across
+ * multiple functions/expressions in a query.
+ *
+ * To add a default without overwriting existing ones
+ * use `getTypeMap()->addDefaults()`
+ *
+ * @param array $types The array of types to set.
+ * @return $this
+ * @see \Cake\Database\TypeMap::setDefaults()
+ */
+ public function setDefaultTypes(array $types)
+ {
+ $this->getTypeMap()->setDefaults($types);
+
+ return $this;
+ }
+
+ /**
+ * Gets default types of current type map.
+ *
+ * @return array
+ */
+ public function getDefaultTypes(): array
+ {
+ return $this->getTypeMap()->getDefaults();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/TypedResultInterface.php b/app/vendor/cakephp/cakephp/src/Database/TypedResultInterface.php
new file mode 100644
index 000000000..434ff08ca
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/TypedResultInterface.php
@@ -0,0 +1,38 @@
+_returnType;
+ }
+
+ /**
+ * Sets the type of the value this object will generate.
+ *
+ * @param string $type The name of the type that is to be returned
+ * @return $this
+ */
+ public function setReturnType(string $type)
+ {
+ $this->_returnType = $type;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/ValueBinder.php b/app/vendor/cakephp/cakephp/src/Database/ValueBinder.php
new file mode 100644
index 000000000..1a63a57d9
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/ValueBinder.php
@@ -0,0 +1,151 @@
+_bindings[$param] = compact('value', 'type') + [
+ 'placeholder' => is_int($param) ? $param : substr($param, 1),
+ ];
+ }
+
+ /**
+ * Creates a unique placeholder name if the token provided does not start with ":"
+ * otherwise, it will return the same string and internally increment the number
+ * of placeholders generated by this object.
+ *
+ * @param string $token string from which the placeholder will be derived from,
+ * if it starts with a colon, then the same string is returned
+ * @return string to be used as a placeholder in a query expression
+ */
+ public function placeholder(string $token): string
+ {
+ $number = $this->_bindingsCount++;
+ if ($token[0] !== ':' && $token !== '?') {
+ $token = sprintf(':%s%s', $token, $number);
+ }
+
+ return $token;
+ }
+
+ /**
+ * Creates unique named placeholders for each of the passed values
+ * and binds them with the specified type.
+ *
+ * @param iterable $values The list of values to be bound
+ * @param string|int|null $type The type with which all values will be bound
+ * @return array with the placeholders to insert in the query
+ */
+ public function generateManyNamed(iterable $values, $type = null): array
+ {
+ $placeholders = [];
+ foreach ($values as $k => $value) {
+ $param = $this->placeholder('c');
+ $this->_bindings[$param] = [
+ 'value' => $value,
+ 'type' => $type,
+ 'placeholder' => substr($param, 1),
+ ];
+ $placeholders[$k] = $param;
+ }
+
+ return $placeholders;
+ }
+
+ /**
+ * Returns all values bound to this expression object at this nesting level.
+ * Subexpression bound values will not be returned with this function.
+ *
+ * @return array
+ */
+ public function bindings(): array
+ {
+ return $this->_bindings;
+ }
+
+ /**
+ * Clears any bindings that were previously registered
+ *
+ * @return void
+ */
+ public function reset(): void
+ {
+ $this->_bindings = [];
+ $this->_bindingsCount = 0;
+ }
+
+ /**
+ * Resets the bindings count without clearing previously bound values
+ *
+ * @return void
+ */
+ public function resetCount(): void
+ {
+ $this->_bindingsCount = 0;
+ }
+
+ /**
+ * Binds all the stored values in this object to the passed statement.
+ *
+ * @param \Cake\Database\StatementInterface $statement The statement to add parameters to.
+ * @return void
+ */
+ public function attachTo(StatementInterface $statement): void
+ {
+ $bindings = $this->bindings();
+ if (empty($bindings)) {
+ return;
+ }
+
+ foreach ($bindings as $b) {
+ $statement->bindValue($b['placeholder'], $b['value'], $b['type']);
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Database/composer.json b/app/vendor/cakephp/cakephp/src/Database/composer.json
new file mode 100644
index 000000000..93251172c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Database/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "cakephp/database",
+ "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "database",
+ "abstraction",
+ "database abstraction",
+ "pdo"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/database/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/database"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/core": "^4.0",
+ "cakephp/datasource": "^4.0"
+ },
+ "suggest": {
+ "cakephp/i18n": "If you are using locale-aware datetime formats or Chronos types."
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Database\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ConnectionInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionInterface.php
new file mode 100644
index 000000000..fcf96b65d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionInterface.php
@@ -0,0 +1,146 @@
+transactional(function ($connection) {
+ * $connection->newQuery()->delete('users')->execute();
+ * });
+ * ```
+ *
+ * @param callable $callback The callback to execute within a transaction.
+ * @return mixed The return value of the callback.
+ * @throws \Exception Will re-throw any exception raised in $callback after
+ * rolling back the transaction.
+ */
+ public function transactional(callable $callback);
+
+ /**
+ * Run an operation with constraints disabled.
+ *
+ * Constraints should be re-enabled after the callback succeeds/fails.
+ *
+ * ### Example:
+ *
+ * ```
+ * $connection->disableConstraints(function ($connection) {
+ * $connection->newQuery()->delete('users')->execute();
+ * });
+ * ```
+ *
+ * @param callable $callback The callback to execute within a transaction.
+ * @return mixed The return value of the callback.
+ * @throws \Exception Will re-throw any exception raised in $callback after
+ * rolling back the transaction.
+ */
+ public function disableConstraints(callable $callback);
+
+ /**
+ * Enable/disable query logging
+ *
+ * @param bool $enable Enable/disable query logging
+ * @return $this
+ */
+ public function enableQueryLogging(bool $enable = true);
+
+ /**
+ * Disable query logging
+ *
+ * @return $this
+ */
+ public function disableQueryLogging();
+
+ /**
+ * Check if query logging is enabled.
+ *
+ * @return bool
+ */
+ public function isQueryLoggingEnabled(): bool;
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ConnectionManager.php b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionManager.php
new file mode 100644
index 000000000..44bf79ec3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionManager.php
@@ -0,0 +1,214 @@
+
+ */
+ protected static $_dsnClassMap = [
+ 'mysql' => Mysql::class,
+ 'postgres' => Postgres::class,
+ 'sqlite' => Sqlite::class,
+ 'sqlserver' => Sqlserver::class,
+ ];
+
+ /**
+ * The ConnectionRegistry used by the manager.
+ *
+ * @var \Cake\Datasource\ConnectionRegistry
+ */
+ protected static $_registry;
+
+ /**
+ * Configure a new connection object.
+ *
+ * The connection will not be constructed until it is first used.
+ *
+ * @param string|array $key The name of the connection config, or an array of multiple configs.
+ * @param array|null $config An array of name => config data for adapter.
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException When trying to modify an existing config.
+ * @see \Cake\Core\StaticConfigTrait::config()
+ */
+ public static function setConfig($key, $config = null): void
+ {
+ if (is_array($config)) {
+ $config['name'] = $key;
+ }
+
+ static::_setConfig($key, $config);
+ }
+
+ /**
+ * Parses a DSN into a valid connection configuration
+ *
+ * This method allows setting a DSN using formatting similar to that used by PEAR::DB.
+ * The following is an example of its usage:
+ *
+ * ```
+ * $dsn = 'mysql://user:pass@localhost/database';
+ * $config = ConnectionManager::parseDsn($dsn);
+ *
+ * $dsn = 'Cake\Database\Driver\Mysql://localhost:3306/database?className=Cake\Database\Connection';
+ * $config = ConnectionManager::parseDsn($dsn);
+ *
+ * $dsn = 'Cake\Database\Connection://localhost:3306/database?driver=Cake\Database\Driver\Mysql';
+ * $config = ConnectionManager::parseDsn($dsn);
+ * ```
+ *
+ * For all classes, the value of `scheme` is set as the value of both the `className` and `driver`
+ * unless they have been otherwise specified.
+ *
+ * Note that query-string arguments are also parsed and set as values in the returned configuration.
+ *
+ * @param string $config The DSN string to convert to a configuration array
+ * @return array The configuration array to be stored after parsing the DSN
+ */
+ public static function parseDsn(string $config): array
+ {
+ $config = static::_parseDsn($config);
+
+ if (isset($config['path']) && empty($config['database'])) {
+ $config['database'] = substr($config['path'], 1);
+ }
+
+ if (empty($config['driver'])) {
+ $config['driver'] = $config['className'];
+ $config['className'] = Connection::class;
+ }
+
+ unset($config['path']);
+
+ return $config;
+ }
+
+ /**
+ * Set one or more connection aliases.
+ *
+ * Connection aliases allow you to rename active connections without overwriting
+ * the aliased connection. This is most useful in the test-suite for replacing
+ * connections with their test variant.
+ *
+ * Defined aliases will take precedence over normal connection names. For example,
+ * if you alias 'default' to 'test', fetching 'default' will always return the 'test'
+ * connection as long as the alias is defined.
+ *
+ * You can remove aliases with ConnectionManager::dropAlias().
+ *
+ * ### Usage
+ *
+ * ```
+ * // Make 'things' resolve to 'test_things' connection
+ * ConnectionManager::alias('test_things', 'things');
+ * ```
+ *
+ * @param string $alias The alias to add. Fetching $source will return $alias when loaded with get.
+ * @param string $source The connection to add an alias to.
+ * @return void
+ * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When aliasing a
+ * connection that does not exist.
+ */
+ public static function alias(string $alias, string $source): void
+ {
+ if (empty(static::$_config[$source]) && empty(static::$_config[$alias])) {
+ throw new MissingDatasourceConfigException(
+ sprintf('Cannot create alias of "%s" as it does not exist.', $alias)
+ );
+ }
+ static::$_aliasMap[$source] = $alias;
+ }
+
+ /**
+ * Drop an alias.
+ *
+ * Removes an alias from ConnectionManager. Fetching the aliased
+ * connection may fail if there is no other connection with that name.
+ *
+ * @param string $name The connection name to remove aliases for.
+ * @return void
+ */
+ public static function dropAlias(string $name): void
+ {
+ unset(static::$_aliasMap[$name]);
+ }
+
+ /**
+ * Get a connection.
+ *
+ * If the connection has not been constructed an instance will be added
+ * to the registry. This method will use any aliases that have been
+ * defined. If you want the original unaliased connections pass `false`
+ * as second parameter.
+ *
+ * @param string $name The connection name.
+ * @param bool $useAliases Set to false to not use aliased connections.
+ * @return \Cake\Datasource\ConnectionInterface A connection object.
+ * @throws \Cake\Datasource\Exception\MissingDatasourceConfigException When config
+ * data is missing.
+ */
+ public static function get(string $name, bool $useAliases = true)
+ {
+ if ($useAliases && isset(static::$_aliasMap[$name])) {
+ $name = static::$_aliasMap[$name];
+ }
+ if (empty(static::$_config[$name])) {
+ throw new MissingDatasourceConfigException(['name' => $name]);
+ }
+ if (empty(static::$_registry)) {
+ static::$_registry = new ConnectionRegistry();
+ }
+ if (isset(static::$_registry->{$name})) {
+ return static::$_registry->{$name};
+ }
+
+ return static::$_registry->load($name, static::$_config[$name]);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ConnectionRegistry.php b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionRegistry.php
new file mode 100644
index 000000000..9bedc0166
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/ConnectionRegistry.php
@@ -0,0 +1,104 @@
+
+ */
+class ConnectionRegistry extends ObjectRegistry
+{
+ /**
+ * Resolve a datasource classname.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class Partial classname to resolve.
+ * @return string|null Either the correct class name or null.
+ * @psalm-return class-string|null
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ return App::className($class, 'Datasource');
+ }
+
+ /**
+ * Throws an exception when a datasource is missing
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class The classname that is missing.
+ * @param string|null $plugin The plugin the datasource is missing in.
+ * @return void
+ * @throws \Cake\Datasource\Exception\MissingDatasourceException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ throw new MissingDatasourceException([
+ 'class' => $class,
+ 'plugin' => $plugin,
+ ]);
+ }
+
+ /**
+ * Create the connection object with the correct settings.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * If a callable is passed as first argument, The returned value of this
+ * function will be the result of the callable.
+ *
+ * @param string|\Cake\Datasource\ConnectionInterface|callable $class The classname or object to make.
+ * @param string $alias The alias of the object.
+ * @param array $config An array of settings to use for the datasource.
+ * @return \Cake\Datasource\ConnectionInterface A connection with the correct settings.
+ */
+ protected function _create($class, string $alias, array $config)
+ {
+ if (is_callable($class)) {
+ return $class($alias);
+ }
+
+ if (is_object($class)) {
+ return $class;
+ }
+
+ unset($config['className']);
+
+ /** @var \Cake\Datasource\ConnectionInterface */
+ return new $class($config);
+ }
+
+ /**
+ * Remove a single adapter from the registry.
+ *
+ * @param string $name The adapter name.
+ * @return $this
+ */
+ public function unload(string $name)
+ {
+ unset($this->_loaded[$name]);
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/EntityInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/EntityInterface.php
new file mode 100644
index 000000000..146ad0dcd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/EntityInterface.php
@@ -0,0 +1,287 @@
+ true`
+ * means that any field not defined in the map will be accessible by default
+ *
+ * @var bool[]
+ */
+ protected $_accessible = ['*' => true];
+
+ /**
+ * The alias of the repository this entity came from
+ *
+ * @var string
+ */
+ protected $_registryAlias = '';
+
+ /**
+ * Magic getter to access fields that have been set in this entity
+ *
+ * @param string $field Name of the field to access
+ * @return mixed
+ */
+ public function &__get(string $field)
+ {
+ return $this->get($field);
+ }
+
+ /**
+ * Magic setter to add or edit a field in this entity
+ *
+ * @param string $field The name of the field to set
+ * @param mixed $value The value to set to the field
+ * @return void
+ */
+ public function __set(string $field, $value): void
+ {
+ $this->set($field, $value);
+ }
+
+ /**
+ * Returns whether this entity contains a field named $field
+ * regardless of if it is empty.
+ *
+ * @param string $field The field to check.
+ * @return bool
+ * @see \Cake\ORM\Entity::has()
+ */
+ public function __isset(string $field): bool
+ {
+ return $this->has($field);
+ }
+
+ /**
+ * Removes a field from this entity
+ *
+ * @param string $field The field to unset
+ * @return void
+ */
+ public function __unset(string $field): void
+ {
+ $this->unset($field);
+ }
+
+ /**
+ * Sets a single field inside this entity.
+ *
+ * ### Example:
+ *
+ * ```
+ * $entity->set('name', 'Andrew');
+ * ```
+ *
+ * It is also possible to mass-assign multiple fields to this entity
+ * with one call by passing a hashed array as fields in the form of
+ * field => value pairs
+ *
+ * ### Example:
+ *
+ * ```
+ * $entity->set(['name' => 'andrew', 'id' => 1]);
+ * echo $entity->name // prints andrew
+ * echo $entity->id // prints 1
+ * ```
+ *
+ * Some times it is handy to bypass setter functions in this entity when assigning
+ * fields. You can achieve this by disabling the `setter` option using the
+ * `$options` parameter:
+ *
+ * ```
+ * $entity->set('name', 'Andrew', ['setter' => false]);
+ * $entity->set(['name' => 'Andrew', 'id' => 1], ['setter' => false]);
+ * ```
+ *
+ * Mass assignment should be treated carefully when accepting user input, by default
+ * entities will guard all fields when fields are assigned in bulk. You can disable
+ * the guarding for a single set call with the `guard` option:
+ *
+ * ```
+ * $entity->set(['name' => 'Andrew', 'id' => 1], ['guard' => false]);
+ * ```
+ *
+ * You do not need to use the guard option when assigning fields individually:
+ *
+ * ```
+ * // No need to use the guard option.
+ * $entity->set('name', 'Andrew');
+ * ```
+ *
+ * @param string|array $field the name of field to set or a list of
+ * fields with their respective values
+ * @param mixed $value The value to set to the field or an array if the
+ * first argument is also an array, in which case will be treated as $options
+ * @param array $options options to be used for setting the field. Allowed option
+ * keys are `setter` and `guard`
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function set($field, $value = null, array $options = [])
+ {
+ if (is_string($field) && $field !== '') {
+ $guard = false;
+ $field = [$field => $value];
+ } else {
+ $guard = true;
+ $options = (array)$value;
+ }
+
+ if (!is_array($field)) {
+ throw new InvalidArgumentException('Cannot set an empty field');
+ }
+ $options += ['setter' => true, 'guard' => $guard];
+
+ foreach ($field as $name => $value) {
+ $name = (string)$name;
+ if ($options['guard'] === true && !$this->isAccessible($name)) {
+ continue;
+ }
+
+ $this->setDirty($name, true);
+
+ if (
+ !array_key_exists($name, $this->_original) &&
+ array_key_exists($name, $this->_fields) &&
+ $this->_fields[$name] !== $value
+ ) {
+ $this->_original[$name] = $this->_fields[$name];
+ }
+
+ if (!$options['setter']) {
+ $this->_fields[$name] = $value;
+ continue;
+ }
+
+ $setter = static::_accessor($name, 'set');
+ if ($setter) {
+ $value = $this->{$setter}($value);
+ }
+ $this->_fields[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the value of a field by name
+ *
+ * @param string $field the name of the field to retrieve
+ * @return mixed
+ * @throws \InvalidArgumentException if an empty field name is passed
+ */
+ public function &get(string $field)
+ {
+ if ($field === '') {
+ throw new InvalidArgumentException('Cannot get an empty field');
+ }
+
+ $value = null;
+ $method = static::_accessor($field, 'get');
+
+ if (isset($this->_fields[$field])) {
+ $value = &$this->_fields[$field];
+ }
+
+ if ($method) {
+ $result = $this->{$method}($value);
+
+ return $result;
+ }
+
+ return $value;
+ }
+
+ /**
+ * Returns the value of an original field by name
+ *
+ * @param string $field the name of the field for which original value is retrieved.
+ * @return mixed
+ * @throws \InvalidArgumentException if an empty field name is passed.
+ */
+ public function getOriginal(string $field)
+ {
+ if (!strlen($field)) {
+ throw new InvalidArgumentException('Cannot get an empty field');
+ }
+ if (array_key_exists($field, $this->_original)) {
+ return $this->_original[$field];
+ }
+
+ return $this->get($field);
+ }
+
+ /**
+ * Gets all original values of the entity.
+ *
+ * @return array
+ */
+ public function getOriginalValues(): array
+ {
+ $originals = $this->_original;
+ $originalKeys = array_keys($originals);
+ foreach ($this->_fields as $key => $value) {
+ if (!in_array($key, $originalKeys, true)) {
+ $originals[$key] = $value;
+ }
+ }
+
+ return $originals;
+ }
+
+ /**
+ * Returns whether this entity contains a field named $field
+ * that contains a non-null value.
+ *
+ * ### Example:
+ *
+ * ```
+ * $entity = new Entity(['id' => 1, 'name' => null]);
+ * $entity->has('id'); // true
+ * $entity->has('name'); // false
+ * $entity->has('last_name'); // false
+ * ```
+ *
+ * You can check multiple fields by passing an array:
+ *
+ * ```
+ * $entity->has(['name', 'last_name']);
+ * ```
+ *
+ * All fields must not be null to get a truthy result.
+ *
+ * When checking multiple fields. All fields must not be null
+ * in order for true to be returned.
+ *
+ * @param string|string[] $field The field or fields to check.
+ * @return bool
+ */
+ public function has($field): bool
+ {
+ foreach ((array)$field as $prop) {
+ if ($this->get($prop) === null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks that a field is empty
+ *
+ * This is not working like the PHP `empty()` function. The method will
+ * return true for:
+ *
+ * - `''` (empty string)
+ * - `null`
+ * - `[]`
+ *
+ * and false in all other cases.
+ *
+ * @param string $field The field to check.
+ * @return bool
+ */
+ public function isEmpty(string $field): bool
+ {
+ $value = $this->get($field);
+ if (
+ $value === null ||
+ (
+ is_array($value) &&
+ empty($value) ||
+ (
+ is_string($value) &&
+ $value === ''
+ )
+ )
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks tha a field has a value.
+ *
+ * This method will return true for
+ *
+ * - Non-empty strings
+ * - Non-empty arrays
+ * - Any object
+ * - Integer, even `0`
+ * - Float, even 0.0
+ *
+ * and false in all other cases.
+ *
+ * @param string $field The field to check.
+ * @return bool
+ */
+ public function hasValue(string $field): bool
+ {
+ return !$this->isEmpty($field);
+ }
+
+ /**
+ * Removes a field or list of fields from this entity
+ *
+ * ### Examples:
+ *
+ * ```
+ * $entity->unset('name');
+ * $entity->unset(['name', 'last_name']);
+ * ```
+ *
+ * @param string|string[] $field The field to unset.
+ * @return $this
+ */
+ public function unset($field)
+ {
+ $field = (array)$field;
+ foreach ($field as $p) {
+ unset($this->_fields[$p], $this->_original[$p], $this->_dirty[$p]);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Removes a field or list of fields from this entity
+ *
+ * @deprecated 4.0.0 Use {@link unset()} instead. Will be removed in 5.0.
+ * @param string|string[] $field The field to unset.
+ * @return $this
+ */
+ public function unsetProperty($field)
+ {
+ deprecationWarning('EntityTrait::unsetProperty() is deprecated. Use unset() instead.');
+
+ return $this->unset($field);
+ }
+
+ /**
+ * Sets hidden fields.
+ *
+ * @param string[] $fields An array of fields to hide from array exports.
+ * @param bool $merge Merge the new fields with the existing. By default false.
+ * @return $this
+ */
+ public function setHidden(array $fields, bool $merge = false)
+ {
+ if ($merge === false) {
+ $this->_hidden = $fields;
+
+ return $this;
+ }
+
+ $fields = array_merge($this->_hidden, $fields);
+ $this->_hidden = array_unique($fields);
+
+ return $this;
+ }
+
+ /**
+ * Gets the hidden fields.
+ *
+ * @return string[]
+ */
+ public function getHidden(): array
+ {
+ return $this->_hidden;
+ }
+
+ /**
+ * Sets the virtual fields on this entity.
+ *
+ * @param string[] $fields An array of fields to treat as virtual.
+ * @param bool $merge Merge the new fields with the existing. By default false.
+ * @return $this
+ */
+ public function setVirtual(array $fields, bool $merge = false)
+ {
+ if ($merge === false) {
+ $this->_virtual = $fields;
+
+ return $this;
+ }
+
+ $fields = array_merge($this->_virtual, $fields);
+ $this->_virtual = array_unique($fields);
+
+ return $this;
+ }
+
+ /**
+ * Gets the virtual fields on this entity.
+ *
+ * @return string[]
+ */
+ public function getVirtual(): array
+ {
+ return $this->_virtual;
+ }
+
+ /**
+ * Gets the list of visible fields.
+ *
+ * The list of visible fields is all standard fields
+ * plus virtual fields minus hidden fields.
+ *
+ * @return string[] A list of fields that are 'visible' in all
+ * representations.
+ */
+ public function getVisible(): array
+ {
+ $fields = array_keys($this->_fields);
+ $fields = array_merge($fields, $this->_virtual);
+
+ return array_diff($fields, $this->_hidden);
+ }
+
+ /**
+ * Returns an array with all the fields that have been set
+ * to this entity
+ *
+ * This method will recursively transform entities assigned to fields
+ * into arrays as well.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ $result = [];
+ foreach ($this->getVisible() as $field) {
+ $value = $this->get($field);
+ if (is_array($value)) {
+ $result[$field] = [];
+ foreach ($value as $k => $entity) {
+ if ($entity instanceof EntityInterface) {
+ $result[$field][$k] = $entity->toArray();
+ } else {
+ $result[$field][$k] = $entity;
+ }
+ }
+ } elseif ($value instanceof EntityInterface) {
+ $result[$field] = $value->toArray();
+ } else {
+ $result[$field] = $value;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the fields that will be serialized as JSON
+ *
+ * @return array
+ */
+ public function jsonSerialize(): array
+ {
+ return $this->extract($this->getVisible());
+ }
+
+ /**
+ * Implements isset($entity);
+ *
+ * @param string $offset The offset to check.
+ * @return bool Success
+ */
+ public function offsetExists($offset): bool
+ {
+ return $this->has($offset);
+ }
+
+ /**
+ * Implements $entity[$offset];
+ *
+ * @param string $offset The offset to get.
+ * @return mixed
+ */
+ public function &offsetGet($offset)
+ {
+ return $this->get($offset);
+ }
+
+ /**
+ * Implements $entity[$offset] = $value;
+ *
+ * @param string $offset The offset to set.
+ * @param mixed $value The value to set.
+ * @return void
+ */
+ public function offsetSet($offset, $value): void
+ {
+ $this->set($offset, $value);
+ }
+
+ /**
+ * Implements unset($result[$offset]);
+ *
+ * @param string $offset The offset to remove.
+ * @return void
+ */
+ public function offsetUnset($offset): void
+ {
+ $this->unset($offset);
+ }
+
+ /**
+ * Fetch accessor method name
+ * Accessor methods (available or not) are cached in $_accessors
+ *
+ * @param string $property the field name to derive getter name from
+ * @param string $type the accessor type ('get' or 'set')
+ * @return string method name or empty string (no method available)
+ */
+ protected static function _accessor(string $property, string $type): string
+ {
+ $class = static::class;
+
+ if (isset(static::$_accessors[$class][$type][$property])) {
+ return static::$_accessors[$class][$type][$property];
+ }
+
+ if (!empty(static::$_accessors[$class])) {
+ return static::$_accessors[$class][$type][$property] = '';
+ }
+
+ if (static::class === Entity::class) {
+ return '';
+ }
+
+ foreach (get_class_methods($class) as $method) {
+ $prefix = substr($method, 1, 3);
+ if ($method[0] !== '_' || ($prefix !== 'get' && $prefix !== 'set')) {
+ continue;
+ }
+ $field = lcfirst(substr($method, 4));
+ $snakeField = Inflector::underscore($field);
+ $titleField = ucfirst($field);
+ static::$_accessors[$class][$prefix][$snakeField] = $method;
+ static::$_accessors[$class][$prefix][$field] = $method;
+ static::$_accessors[$class][$prefix][$titleField] = $method;
+ }
+
+ if (!isset(static::$_accessors[$class][$type][$property])) {
+ static::$_accessors[$class][$type][$property] = '';
+ }
+
+ return static::$_accessors[$class][$type][$property];
+ }
+
+ /**
+ * Returns an array with the requested fields
+ * stored in this entity, indexed by field name
+ *
+ * @param string[] $fields list of fields to be returned
+ * @param bool $onlyDirty Return the requested field only if it is dirty
+ * @return array
+ */
+ public function extract(array $fields, bool $onlyDirty = false): array
+ {
+ $result = [];
+ foreach ($fields as $field) {
+ if (!$onlyDirty || $this->isDirty($field)) {
+ $result[$field] = $this->get($field);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns an array with the requested original fields
+ * stored in this entity, indexed by field name.
+ *
+ * Fields that are unchanged from their original value will be included in the
+ * return of this method.
+ *
+ * @param string[] $fields List of fields to be returned
+ * @return array
+ */
+ public function extractOriginal(array $fields): array
+ {
+ $result = [];
+ foreach ($fields as $field) {
+ $result[$field] = $this->getOriginal($field);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns an array with only the original fields
+ * stored in this entity, indexed by field name.
+ *
+ * This method will only return fields that have been modified since
+ * the entity was built. Unchanged fields will be omitted.
+ *
+ * @param string[] $fields List of fields to be returned
+ * @return array
+ */
+ public function extractOriginalChanged(array $fields): array
+ {
+ $result = [];
+ foreach ($fields as $field) {
+ $original = $this->getOriginal($field);
+ if ($original !== $this->get($field)) {
+ $result[$field] = $original;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Sets the dirty status of a single field.
+ *
+ * @param string $field the field to set or check status for
+ * @param bool $isDirty true means the field was changed, false means
+ * it was not changed. Defaults to true.
+ * @return $this
+ */
+ public function setDirty(string $field, bool $isDirty = true)
+ {
+ if ($isDirty === false) {
+ unset($this->_dirty[$field]);
+
+ return $this;
+ }
+
+ $this->_dirty[$field] = true;
+ unset($this->_errors[$field], $this->_invalid[$field]);
+
+ return $this;
+ }
+
+ /**
+ * Checks if the entity is dirty or if a single field of it is dirty.
+ *
+ * @param string|null $field The field to check the status for. Null for the whole entity.
+ * @return bool Whether the field was changed or not
+ */
+ public function isDirty(?string $field = null): bool
+ {
+ if ($field === null) {
+ return !empty($this->_dirty);
+ }
+
+ return isset($this->_dirty[$field]);
+ }
+
+ /**
+ * Gets the dirty fields.
+ *
+ * @return string[]
+ */
+ public function getDirty(): array
+ {
+ return array_keys($this->_dirty);
+ }
+
+ /**
+ * Sets the entire entity as clean, which means that it will appear as
+ * no fields being modified or added at all. This is an useful call
+ * for an initial object hydration
+ *
+ * @return void
+ */
+ public function clean(): void
+ {
+ $this->_dirty = [];
+ $this->_errors = [];
+ $this->_invalid = [];
+ $this->_original = [];
+ }
+
+ /**
+ * Set the status of this entity.
+ *
+ * Using `true` means that the entity has not been persisted in the database,
+ * `false` that it already is.
+ *
+ * @param bool $new Indicate whether or not this entity has been persisted.
+ * @return $this
+ */
+ public function setNew(bool $new)
+ {
+ if ($new) {
+ foreach ($this->_fields as $k => $p) {
+ $this->_dirty[$k] = true;
+ }
+ }
+
+ $this->_new = $new;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether or not this entity has already been persisted.
+ *
+ * @return bool Whether or not the entity has been persisted.
+ */
+ public function isNew(): bool
+ {
+ if (func_num_args()) {
+ deprecationWarning('Using isNew() as setter is deprecated. Use setNew() instead.');
+
+ $this->setNew(func_get_arg(0));
+ }
+
+ return $this->_new;
+ }
+
+ /**
+ * Returns whether this entity has errors.
+ *
+ * @param bool $includeNested true will check nested entities for hasErrors()
+ * @return bool
+ */
+ public function hasErrors(bool $includeNested = true): bool
+ {
+ if (Hash::filter($this->_errors)) {
+ return true;
+ }
+
+ if ($includeNested === false) {
+ return false;
+ }
+
+ foreach ($this->_fields as $field) {
+ if ($this->_readHasErrors($field)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns all validation errors.
+ *
+ * @return array
+ */
+ public function getErrors(): array
+ {
+ $diff = array_diff_key($this->_fields, $this->_errors);
+
+ return $this->_errors + (new Collection($diff))
+ ->filter(function ($value) {
+ return is_array($value) || $value instanceof EntityInterface;
+ })
+ ->map(function ($value) {
+ return $this->_readError($value);
+ })
+ ->filter()
+ ->toArray();
+ }
+
+ /**
+ * Returns validation errors of a field
+ *
+ * @param string $field Field name to get the errors from
+ * @return array
+ */
+ public function getError(string $field): array
+ {
+ $errors = $this->_errors[$field] ?? [];
+ if ($errors) {
+ return $errors;
+ }
+
+ return $this->_nestedErrors($field);
+ }
+
+ /**
+ * Sets error messages to the entity
+ *
+ * ## Example
+ *
+ * ```
+ * // Sets the error messages for multiple fields at once
+ * $entity->setErrors(['salary' => ['message'], 'name' => ['another message']]);
+ * ```
+ *
+ * @param array $errors The array of errors to set.
+ * @param bool $overwrite Whether or not to overwrite pre-existing errors for $fields
+ * @return $this
+ */
+ public function setErrors(array $errors, bool $overwrite = false)
+ {
+ if ($overwrite) {
+ foreach ($errors as $f => $error) {
+ $this->_errors[$f] = (array)$error;
+ }
+
+ return $this;
+ }
+
+ foreach ($errors as $f => $error) {
+ $this->_errors += [$f => []];
+
+ // String messages are appended to the list,
+ // while more complex error structures need their
+ // keys preserved for nested validator.
+ if (is_string($error)) {
+ $this->_errors[$f][] = $error;
+ } else {
+ foreach ($error as $k => $v) {
+ $this->_errors[$f][$k] = $v;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets errors for a single field
+ *
+ * ### Example
+ *
+ * ```
+ * // Sets the error messages for a single field
+ * $entity->setError('salary', ['must be numeric', 'must be a positive number']);
+ * ```
+ *
+ * @param string $field The field to get errors for, or the array of errors to set.
+ * @param string|array $errors The errors to be set for $field
+ * @param bool $overwrite Whether or not to overwrite pre-existing errors for $field
+ * @return $this
+ */
+ public function setError(string $field, $errors, bool $overwrite = false)
+ {
+ if (is_string($errors)) {
+ $errors = [$errors];
+ }
+
+ return $this->setErrors([$field => $errors], $overwrite);
+ }
+
+ /**
+ * Auxiliary method for getting errors in nested entities
+ *
+ * @param string $field the field in this entity to check for errors
+ * @return array errors in nested entity if any
+ */
+ protected function _nestedErrors(string $field): array
+ {
+ // Only one path element, check for nested entity with error.
+ if (strpos($field, '.') === false) {
+ return $this->_readError($this->get($field));
+ }
+ // Try reading the errors data with field as a simple path
+ $error = Hash::get($this->_errors, $field);
+ if ($error !== null) {
+ return $error;
+ }
+ $path = explode('.', $field);
+
+ // Traverse down the related entities/arrays for
+ // the relevant entity.
+ $entity = $this;
+ $len = count($path);
+ while ($len) {
+ $part = array_shift($path);
+ $len = count($path);
+ $val = null;
+ if ($entity instanceof EntityInterface) {
+ $val = $entity->get($part);
+ } elseif (is_array($entity)) {
+ $val = $entity[$part] ?? false;
+ }
+
+ if (
+ is_array($val) ||
+ $val instanceof Traversable ||
+ $val instanceof EntityInterface
+ ) {
+ $entity = $val;
+ } else {
+ $path[] = $part;
+ break;
+ }
+ }
+ if (count($path) <= 1) {
+ return $this->_readError($entity, array_pop($path));
+ }
+
+ return [];
+ }
+
+ /**
+ * Reads if there are errors for one or many objects.
+ *
+ * @param array|\Cake\Datasource\EntityInterface $object The object to read errors from.
+ * @return bool
+ */
+ protected function _readHasErrors($object): bool
+ {
+ if ($object instanceof EntityInterface && $object->hasErrors()) {
+ return true;
+ }
+
+ if (is_array($object)) {
+ foreach ($object as $value) {
+ if ($this->_readHasErrors($value)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Read the error(s) from one or many objects.
+ *
+ * @param iterable|\Cake\Datasource\EntityInterface $object The object to read errors from.
+ * @param string|null $path The field name for errors.
+ * @return array
+ */
+ protected function _readError($object, $path = null): array
+ {
+ if ($path !== null && $object instanceof EntityInterface) {
+ return $object->getError($path);
+ }
+ if ($object instanceof EntityInterface) {
+ return $object->getErrors();
+ }
+ if (is_iterable($object)) {
+ $array = array_map(function ($val) {
+ if ($val instanceof EntityInterface) {
+ return $val->getErrors();
+ }
+ }, (array)$object);
+
+ return array_filter($array);
+ }
+
+ return [];
+ }
+
+ /**
+ * Get a list of invalid fields and their data for errors upon validation/patching
+ *
+ * @return array
+ */
+ public function getInvalid(): array
+ {
+ return $this->_invalid;
+ }
+
+ /**
+ * Get a single value of an invalid field. Returns null if not set.
+ *
+ * @param string $field The name of the field.
+ * @return mixed|null
+ */
+ public function getInvalidField(string $field)
+ {
+ return $this->_invalid[$field] ?? null;
+ }
+
+ /**
+ * Set fields as invalid and not patchable into the entity.
+ *
+ * This is useful for batch operations when one needs to get the original value for an error message after patching.
+ * This value could not be patched into the entity and is simply copied into the _invalid property for debugging
+ * purposes or to be able to log it away.
+ *
+ * @param array $fields The values to set.
+ * @param bool $overwrite Whether or not to overwrite pre-existing values for $field.
+ * @return $this
+ */
+ public function setInvalid(array $fields, bool $overwrite = false)
+ {
+ foreach ($fields as $field => $value) {
+ if ($overwrite === true) {
+ $this->_invalid[$field] = $value;
+ continue;
+ }
+ $this->_invalid += [$field => $value];
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets a field as invalid and not patchable into the entity.
+ *
+ * @param string $field The value to set.
+ * @param mixed $value The invalid value to be set for $field.
+ * @return $this
+ */
+ public function setInvalidField(string $field, $value)
+ {
+ $this->_invalid[$field] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Stores whether or not a field value can be changed or set in this entity.
+ * The special field `*` can also be marked as accessible or protected, meaning
+ * that any other field specified before will take its value. For example
+ * `$entity->setAccess('*', true)` means that any field not specified already
+ * will be accessible by default.
+ *
+ * You can also call this method with an array of fields, in which case they
+ * will each take the accessibility value specified in the second argument.
+ *
+ * ### Example:
+ *
+ * ```
+ * $entity->setAccess('id', true); // Mark id as not protected
+ * $entity->setAccess('author_id', false); // Mark author_id as protected
+ * $entity->setAccess(['id', 'user_id'], true); // Mark both fields as accessible
+ * $entity->setAccess('*', false); // Mark all fields as protected
+ * ```
+ *
+ * @param string|array $field Single or list of fields to change its accessibility
+ * @param bool $set True marks the field as accessible, false will
+ * mark it as protected.
+ * @return $this
+ */
+ public function setAccess($field, bool $set)
+ {
+ if ($field === '*') {
+ $this->_accessible = array_map(function ($p) use ($set) {
+ return $set;
+ }, $this->_accessible);
+ $this->_accessible['*'] = $set;
+
+ return $this;
+ }
+
+ foreach ((array)$field as $prop) {
+ $this->_accessible[$prop] = $set;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the raw accessible configuration for this entity.
+ * The `*` wildcard refers to all fields.
+ *
+ * @return bool[]
+ */
+ public function getAccessible(): array
+ {
+ return $this->_accessible;
+ }
+
+ /**
+ * Checks if a field is accessible
+ *
+ * ### Example:
+ *
+ * ```
+ * $entity->isAccessible('id'); // Returns whether it can be set or not
+ * ```
+ *
+ * @param string $field Field name to check
+ * @return bool
+ */
+ public function isAccessible(string $field): bool
+ {
+ $value = $this->_accessible[$field] ?? null;
+
+ return ($value === null && !empty($this->_accessible['*'])) || $value;
+ }
+
+ /**
+ * Returns the alias of the repository from which this entity came from.
+ *
+ * @return string
+ */
+ public function getSource(): string
+ {
+ return $this->_registryAlias;
+ }
+
+ /**
+ * Sets the source alias
+ *
+ * @param string $alias the alias of the repository
+ * @return $this
+ */
+ public function setSource(string $alias)
+ {
+ $this->_registryAlias = $alias;
+
+ return $this;
+ }
+
+ /**
+ * Returns a string representation of this object in a human readable format.
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ return (string)json_encode($this, JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $fields = $this->_fields;
+ foreach ($this->_virtual as $field) {
+ $fields[$field] = $this->$field;
+ }
+
+ return $fields + [
+ '[new]' => $this->isNew(),
+ '[accessible]' => $this->_accessible,
+ '[dirty]' => $this->_dirty,
+ '[original]' => $this->_original,
+ '[virtual]' => $this->_virtual,
+ '[hasErrors]' => $this->hasErrors(),
+ '[errors]' => $this->_errors,
+ '[invalid]' => $this->_invalid,
+ '[repository]' => $this->_registryAlias,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/Exception/InvalidPrimaryKeyException.php b/app/vendor/cakephp/cakephp/src/Datasource/Exception/InvalidPrimaryKeyException.php
new file mode 100644
index 000000000..ac7a72b1b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/Exception/InvalidPrimaryKeyException.php
@@ -0,0 +1,26 @@
+instances[$alias])) {
+ if (!empty($storeOptions) && $this->options[$alias] !== $storeOptions) {
+ throw new RuntimeException(sprintf(
+ 'You cannot configure "%s", it already exists in the registry.',
+ $alias
+ ));
+ }
+
+ return $this->instances[$alias];
+ }
+
+ $this->options[$alias] = $storeOptions;
+
+ return $this->instances[$alias] = $this->createInstance($alias, $options);
+ }
+
+ /**
+ * Create an instance of a given classname.
+ *
+ * @param string $alias Repository alias.
+ * @param array $options The options you want to build the instance with.
+ * @return \Cake\Datasource\RepositoryInterface
+ */
+ abstract protected function createInstance(string $alias, array $options);
+
+ /**
+ * @inheritDoc
+ */
+ public function set(string $alias, RepositoryInterface $repository)
+ {
+ return $this->instances[$alias] = $repository;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function exists(string $alias): bool
+ {
+ return isset($this->instances[$alias]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function remove(string $alias): void
+ {
+ unset(
+ $this->instances[$alias],
+ $this->options[$alias]
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clear(): void
+ {
+ $this->instances = [];
+ $this->options = [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/Locator/LocatorInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/Locator/LocatorInterface.php
new file mode 100644
index 000000000..1d63a4560
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/Locator/LocatorInterface.php
@@ -0,0 +1,68 @@
+modelClass === null) {
+ $this->modelClass = $name;
+ }
+ }
+
+ /**
+ * Loads and constructs repository objects required by this object
+ *
+ * Typically used to load ORM Table objects as required. Can
+ * also be used to load other types of repository objects your application uses.
+ *
+ * If a repository provider does not return an object a MissingModelException will
+ * be thrown.
+ *
+ * @param string|null $modelClass Name of model class to load. Defaults to $this->modelClass.
+ * The name can be an alias like `'Post'` or FQCN like `App\Model\Table\PostsTable::class`.
+ * @param string|null $modelType The type of repository to load. Defaults to the getModelType() value.
+ * @return \Cake\Datasource\RepositoryInterface The model instance created.
+ * @throws \Cake\Datasource\Exception\MissingModelException If the model class cannot be found.
+ * @throws \UnexpectedValueException If $modelClass argument is not provided
+ * and ModelAwareTrait::$modelClass property value is empty.
+ */
+ public function loadModel(?string $modelClass = null, ?string $modelType = null): RepositoryInterface
+ {
+ $modelClass = $modelClass ?? $this->modelClass;
+ if (empty($modelClass)) {
+ throw new UnexpectedValueException('Default modelClass is empty');
+ }
+ $modelType = $modelType ?? $this->getModelType();
+
+ $options = [];
+ if (strpos($modelClass, '\\') === false) {
+ [, $alias] = pluginSplit($modelClass, true);
+ } else {
+ $options['className'] = $modelClass;
+ /** @psalm-suppress PossiblyFalseOperand */
+ $alias = substr(
+ $modelClass,
+ strrpos($modelClass, '\\') + 1,
+ -strlen($modelType)
+ );
+ $modelClass = $alias;
+ }
+
+ if (isset($this->{$alias})) {
+ return $this->{$alias};
+ }
+
+ $factory = $this->_modelFactories[$modelType] ?? FactoryLocator::get($modelType);
+ if ($factory instanceof LocatorInterface) {
+ $this->{$alias} = $factory->get($modelClass, $options);
+ } else {
+ $this->{$alias} = $factory($modelClass, $options);
+ }
+
+ if (!$this->{$alias}) {
+ throw new MissingModelException([$modelClass, $modelType]);
+ }
+
+ return $this->{$alias};
+ }
+
+ /**
+ * Override a existing callable to generate repositories of a given type.
+ *
+ * @param string $type The name of the repository type the factory function is for.
+ * @param callable|\Cake\Datasource\Locator\LocatorInterface $factory The factory function used to create instances.
+ * @return void
+ */
+ public function modelFactory(string $type, $factory): void
+ {
+ if (!$factory instanceof LocatorInterface && !is_callable($factory)) {
+ throw new InvalidArgumentException(sprintf(
+ '`$factory` must be an instance of Cake\Datasource\Locator\LocatorInterface or a callable.'
+ . ' Got type `%s` instead.',
+ getTypeName($factory)
+ ));
+ }
+
+ $this->_modelFactories[$type] = $factory;
+ }
+
+ /**
+ * Get the model type to be used by this class
+ *
+ * @return string
+ */
+ public function getModelType(): string
+ {
+ return $this->_modelType;
+ }
+
+ /**
+ * Set the model type to be used by this class
+ *
+ * @param string $modelType The model type
+ * @return $this
+ */
+ public function setModelType(string $modelType)
+ {
+ $this->_modelType = $modelType;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/Paginator.php b/app/vendor/cakephp/cakephp/src/Datasource/Paginator.php
new file mode 100644
index 000000000..8c67c9858
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/Paginator.php
@@ -0,0 +1,679 @@
+ 1,
+ 'limit' => 20,
+ 'maxLimit' => 100,
+ 'allowedParameters' => ['limit', 'sort', 'page', 'direction'],
+ ];
+
+ /**
+ * Paging params after pagination operation is done.
+ *
+ * @var array
+ */
+ protected $_pagingParams = [];
+
+ /**
+ * Handles automatic pagination of model records.
+ *
+ * ### Configuring pagination
+ *
+ * When calling `paginate()` you can use the $settings parameter to pass in
+ * pagination settings. These settings are used to build the queries made
+ * and control other pagination settings.
+ *
+ * If your settings contain a key with the current table's alias. The data
+ * inside that key will be used. Otherwise the top level configuration will
+ * be used.
+ *
+ * ```
+ * $settings = [
+ * 'limit' => 20,
+ * 'maxLimit' => 100
+ * ];
+ * $results = $paginator->paginate($table, $settings);
+ * ```
+ *
+ * The above settings will be used to paginate any repository. You can configure
+ * repository specific settings by keying the settings with the repository alias.
+ *
+ * ```
+ * $settings = [
+ * 'Articles' => [
+ * 'limit' => 20,
+ * 'maxLimit' => 100
+ * ],
+ * 'Comments' => [ ... ]
+ * ];
+ * $results = $paginator->paginate($table, $settings);
+ * ```
+ *
+ * This would allow you to have different pagination settings for
+ * `Articles` and `Comments` repositories.
+ *
+ * ### Controlling sort fields
+ *
+ * By default CakePHP will automatically allow sorting on any column on the
+ * repository object being paginated. Often times you will want to allow
+ * sorting on either associated columns or calculated fields. In these cases
+ * you will need to define an allowed list of all the columns you wish to allow
+ * sorting on. You can define the allowed sort fields in the `$settings` parameter:
+ *
+ * ```
+ * $settings = [
+ * 'Articles' => [
+ * 'finder' => 'custom',
+ * 'sortableFields' => ['title', 'author_id', 'comment_count'],
+ * ]
+ * ];
+ * ```
+ *
+ * Passing an empty array as sortableFields disallows sorting altogether.
+ *
+ * ### Paginating with custom finders
+ *
+ * You can paginate with any find type defined on your table using the
+ * `finder` option.
+ *
+ * ```
+ * $settings = [
+ * 'Articles' => [
+ * 'finder' => 'popular'
+ * ]
+ * ];
+ * $results = $paginator->paginate($table, $settings);
+ * ```
+ *
+ * Would paginate using the `find('popular')` method.
+ *
+ * You can also pass an already created instance of a query to this method:
+ *
+ * ```
+ * $query = $this->Articles->find('popular')->matching('Tags', function ($q) {
+ * return $q->where(['name' => 'CakePHP'])
+ * });
+ * $results = $paginator->paginate($query);
+ * ```
+ *
+ * ### Scoping Request parameters
+ *
+ * By using request parameter scopes you can paginate multiple queries in
+ * the same controller action:
+ *
+ * ```
+ * $articles = $paginator->paginate($articlesQuery, ['scope' => 'articles']);
+ * $tags = $paginator->paginate($tagsQuery, ['scope' => 'tags']);
+ * ```
+ *
+ * Each of the above queries will use different query string parameter sets
+ * for pagination data. An example URL paginating both results would be:
+ *
+ * ```
+ * /dashboard?articles[page]=1&tags[page]=2
+ * ```
+ *
+ * @param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The repository or query
+ * to paginate.
+ * @param array $params Request params
+ * @param array $settings The settings/configuration used for pagination.
+ * @return \Cake\Datasource\ResultSetInterface Query results
+ * @throws \Cake\Datasource\Exception\PageOutOfBoundsException
+ */
+ public function paginate(object $object, array $params = [], array $settings = []): ResultSetInterface
+ {
+ $query = null;
+ if ($object instanceof QueryInterface) {
+ $query = $object;
+ $object = $query->getRepository();
+ if ($object === null) {
+ throw new CakeException('No repository set for query.');
+ }
+ }
+
+ $data = $this->extractData($object, $params, $settings);
+ $query = $this->getQuery($object, $query, $data);
+
+ $cleanQuery = clone $query;
+ $results = $query->all();
+ $data['numResults'] = count($results);
+ $data['count'] = $this->getCount($cleanQuery, $data);
+
+ $pagingParams = $this->buildParams($data);
+ $alias = $object->getAlias();
+ $this->_pagingParams = [$alias => $pagingParams];
+ if ($pagingParams['requestedPage'] > $pagingParams['page']) {
+ throw new PageOutOfBoundsException([
+ 'requestedPage' => $pagingParams['requestedPage'],
+ 'pagingParams' => $this->_pagingParams,
+ ]);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Get query for fetching paginated results.
+ *
+ * @param \Cake\Datasource\RepositoryInterface $object Repository instance.
+ * @param \Cake\Datasource\QueryInterface|null $query Query Instance.
+ * @param array $data Pagination data.
+ * @return \Cake\Datasource\QueryInterface
+ */
+ protected function getQuery(RepositoryInterface $object, ?QueryInterface $query = null, array $data): QueryInterface
+ {
+ if ($query === null) {
+ $query = $object->find($data['finder'], $data['options']);
+ } else {
+ $query->applyOptions($data['options']);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Get total count of records.
+ *
+ * @param \Cake\Datasource\QueryInterface $query Query instance.
+ * @param array $data Pagination data.
+ * @return int|null
+ */
+ protected function getCount(QueryInterface $query, array $data): ?int
+ {
+ return $query->count();
+ }
+
+ /**
+ * Extract pagination data needed
+ *
+ * @param \Cake\Datasource\RepositoryInterface $object The repository object.
+ * @param array $params Request params
+ * @param array $settings The settings/configuration used for pagination.
+ * @return array Array with keys 'defaults', 'options' and 'finder'
+ */
+ protected function extractData(RepositoryInterface $object, array $params, array $settings): array
+ {
+ $alias = $object->getAlias();
+ $defaults = $this->getDefaults($alias, $settings);
+ $options = $this->mergeOptions($params, $defaults);
+ $options = $this->validateSort($object, $options);
+ $options = $this->checkLimit($options);
+
+ $options += ['page' => 1, 'scope' => null];
+ $options['page'] = (int)$options['page'] < 1 ? 1 : (int)$options['page'];
+ [$finder, $options] = $this->_extractFinder($options);
+
+ return compact('defaults', 'options', 'finder');
+ }
+
+ /**
+ * Build pagination params.
+ *
+ * @param array $data Paginator data containing keys 'options',
+ * 'count', 'defaults', 'finder', 'numResults'.
+ * @return array Paging params.
+ */
+ protected function buildParams(array $data): array
+ {
+ $limit = $data['options']['limit'];
+
+ $paging = [
+ 'count' => $data['count'],
+ 'current' => $data['numResults'],
+ 'perPage' => $limit,
+ 'page' => $data['options']['page'],
+ 'requestedPage' => $data['options']['page'],
+ ];
+
+ $paging = $this->addPageCountParams($paging, $data);
+ $paging = $this->addStartEndParams($paging, $data);
+ $paging = $this->addPrevNextParams($paging, $data);
+ $paging = $this->addSortingParams($paging, $data);
+
+ $paging += [
+ 'limit' => $data['defaults']['limit'] != $limit ? $limit : null,
+ 'scope' => $data['options']['scope'],
+ 'finder' => $data['finder'],
+ ];
+
+ return $paging;
+ }
+
+ /**
+ * Add "page" and "pageCount" params.
+ *
+ * @param array $params Paging params.
+ * @param array $data Paginator data.
+ * @return array Updated params.
+ */
+ protected function addPageCountParams(array $params, array $data): array
+ {
+ $page = $params['page'];
+ $pageCount = 0;
+
+ if ($params['count'] !== null) {
+ $pageCount = max((int)ceil($params['count'] / $params['perPage']), 1);
+ $page = min($page, $pageCount);
+ } elseif ($params['current'] === 0 && $params['requestedPage'] > 1) {
+ $page = 1;
+ }
+
+ $params['page'] = $page;
+ $params['pageCount'] = $pageCount;
+
+ return $params;
+ }
+
+ /**
+ * Add "start" and "end" params.
+ *
+ * @param array $params Paging params.
+ * @param array $data Paginator data.
+ * @return array Updated params.
+ */
+ protected function addStartEndParams(array $params, array $data): array
+ {
+ $start = $end = 0;
+
+ if ($params['current'] > 0) {
+ $start = (($params['page'] - 1) * $params['perPage']) + 1;
+ $end = $start + $params['current'] - 1;
+ }
+
+ $params['start'] = $start;
+ $params['end'] = $end;
+
+ return $params;
+ }
+
+ /**
+ * Add "prevPage" and "nextPage" params.
+ *
+ * @param array $params Paginator params.
+ * @param array $data Paging data.
+ * @return array Updated params.
+ */
+ protected function addPrevNextParams(array $params, array $data): array
+ {
+ $params['prevPage'] = $params['page'] > 1;
+ if ($params['count'] === null) {
+ $params['nextPage'] = true;
+ } else {
+ $params['nextPage'] = $params['count'] > $params['page'] * $params['perPage'];
+ }
+
+ return $params;
+ }
+
+ /**
+ * Add sorting / ordering params.
+ *
+ * @param array $params Paginator params.
+ * @param array $data Paging data.
+ * @return array Updated params.
+ */
+ protected function addSortingParams(array $params, array $data): array
+ {
+ $defaults = $data['defaults'];
+ $order = (array)$data['options']['order'];
+ $sortDefault = $directionDefault = false;
+
+ if (!empty($defaults['order']) && count($defaults['order']) === 1) {
+ $sortDefault = key($defaults['order']);
+ $directionDefault = current($defaults['order']);
+ }
+
+ $params += [
+ 'sort' => $data['options']['sort'],
+ 'direction' => isset($data['options']['sort']) && count($order) ? current($order) : null,
+ 'sortDefault' => $sortDefault,
+ 'directionDefault' => $directionDefault,
+ 'completeSort' => $order,
+ ];
+
+ return $params;
+ }
+
+ /**
+ * Extracts the finder name and options out of the provided pagination options.
+ *
+ * @param array $options the pagination options.
+ * @return array An array containing in the first position the finder name
+ * and in the second the options to be passed to it.
+ */
+ protected function _extractFinder(array $options): array
+ {
+ $type = !empty($options['finder']) ? $options['finder'] : 'all';
+ unset($options['finder'], $options['maxLimit']);
+
+ if (is_array($type)) {
+ $options = (array)current($type) + $options;
+ $type = key($type);
+ }
+
+ return [$type, $options];
+ }
+
+ /**
+ * Get paging params after pagination operation.
+ *
+ * @return array
+ */
+ public function getPagingParams(): array
+ {
+ return $this->_pagingParams;
+ }
+
+ /**
+ * Shim method for reading the deprecated whitelist or allowedParameters options
+ *
+ * @return string[]
+ */
+ protected function getAllowedParameters(): array
+ {
+ $allowed = $this->getConfig('allowedParameters');
+ if (!$allowed) {
+ $allowed = [];
+ }
+ $whitelist = $this->getConfig('whitelist');
+ if ($whitelist) {
+ deprecationWarning('The `whitelist` option is deprecated. Use the `allowedParameters` option instead.');
+
+ return array_merge($allowed, $whitelist);
+ }
+
+ return $allowed;
+ }
+
+ /**
+ * Shim method for reading the deprecated sortWhitelist or sortableFields options.
+ *
+ * @param array $config The configuration data to coalesce and emit warnings on.
+ * @return string[]|null
+ */
+ protected function getSortableFields(array $config): ?array
+ {
+ $allowed = $config['sortableFields'] ?? null;
+ if ($allowed !== null) {
+ return $allowed;
+ }
+ $deprecated = $config['sortWhitelist'] ?? null;
+ if ($deprecated !== null) {
+ deprecationWarning('The `sortWhitelist` option is deprecated. Use `sortableFields` instead.');
+ }
+
+ return $deprecated;
+ }
+
+ /**
+ * Merges the various options that Paginator uses.
+ * Pulls settings together from the following places:
+ *
+ * - General pagination settings
+ * - Model specific settings.
+ * - Request parameters
+ *
+ * The result of this method is the aggregate of all the option sets
+ * combined together. You can change config value `allowedParameters` to modify
+ * which options/values can be set using request parameters.
+ *
+ * @param array $params Request params.
+ * @param array $settings The settings to merge with the request data.
+ * @return array Array of merged options.
+ */
+ public function mergeOptions(array $params, array $settings): array
+ {
+ if (!empty($settings['scope'])) {
+ $scope = $settings['scope'];
+ $params = !empty($params[$scope]) ? (array)$params[$scope] : [];
+ }
+
+ $allowed = $this->getAllowedParameters();
+ $params = array_intersect_key($params, array_flip($allowed));
+
+ return array_merge($settings, $params);
+ }
+
+ /**
+ * Get the settings for a $model. If there are no settings for a specific
+ * repository, the general settings will be used.
+ *
+ * @param string $alias Model name to get settings for.
+ * @param array $settings The settings which is used for combining.
+ * @return array An array of pagination settings for a model,
+ * or the general settings.
+ */
+ public function getDefaults(string $alias, array $settings): array
+ {
+ if (isset($settings[$alias])) {
+ $settings = $settings[$alias];
+ }
+
+ $defaults = $this->getConfig();
+ $defaults['whitelist'] = $defaults['allowedParameters'] = $this->getAllowedParameters();
+
+ $maxLimit = $settings['maxLimit'] ?? $defaults['maxLimit'];
+ $limit = $settings['limit'] ?? $defaults['limit'];
+
+ if ($limit > $maxLimit) {
+ $limit = $maxLimit;
+ }
+
+ $settings['maxLimit'] = $maxLimit;
+ $settings['limit'] = $limit;
+
+ return $settings + $defaults;
+ }
+
+ /**
+ * Validate that the desired sorting can be performed on the $object.
+ *
+ * Only fields or virtualFields can be sorted on. The direction param will
+ * also be sanitized. Lastly sort + direction keys will be converted into
+ * the model friendly order key.
+ *
+ * You can use the allowedParameters option to control which columns/fields are
+ * available for sorting via URL parameters. This helps prevent users from ordering large
+ * result sets on un-indexed values.
+ *
+ * If you need to sort on associated columns or synthetic properties you
+ * will need to use the `sortableFields` option.
+ *
+ * Any columns listed in the allowed sort fields will be implicitly trusted.
+ * You can use this to sort on synthetic columns, or columns added in custom
+ * find operations that may not exist in the schema.
+ *
+ * The default order options provided to paginate() will be merged with the user's
+ * requested sorting field/direction.
+ *
+ * @param \Cake\Datasource\RepositoryInterface $object Repository object.
+ * @param array $options The pagination options being used for this request.
+ * @return array An array of options with sort + direction removed and
+ * replaced with order if possible.
+ */
+ public function validateSort(RepositoryInterface $object, array $options): array
+ {
+ if (isset($options['sort'])) {
+ $direction = null;
+ if (isset($options['direction'])) {
+ $direction = strtolower($options['direction']);
+ }
+ if (!in_array($direction, ['asc', 'desc'], true)) {
+ $direction = 'asc';
+ }
+
+ $order = isset($options['order']) && is_array($options['order']) ? $options['order'] : [];
+ if ($order && $options['sort'] && strpos($options['sort'], '.') === false) {
+ $order = $this->_removeAliases($order, $object->getAlias());
+ }
+
+ $options['order'] = [$options['sort'] => $direction] + $order;
+ } else {
+ $options['sort'] = null;
+ }
+ unset($options['direction']);
+
+ if (empty($options['order'])) {
+ $options['order'] = [];
+ }
+ if (!is_array($options['order'])) {
+ return $options;
+ }
+
+ $sortAllowed = false;
+ $allowed = $this->getSortableFields($options);
+ if ($allowed !== null) {
+ $options['sortableFields'] = $options['sortWhitelist'] = $allowed;
+
+ $field = key($options['order']);
+ $sortAllowed = in_array($field, $allowed, true);
+ if (!$sortAllowed) {
+ $options['order'] = [];
+ $options['sort'] = null;
+
+ return $options;
+ }
+ }
+
+ if (
+ $options['sort'] === null
+ && count($options['order']) === 1
+ && !is_numeric(key($options['order']))
+ ) {
+ $options['sort'] = key($options['order']);
+ }
+
+ $options['order'] = $this->_prefix($object, $options['order'], $sortAllowed);
+
+ return $options;
+ }
+
+ /**
+ * Remove alias if needed.
+ *
+ * @param array $fields Current fields
+ * @param string $model Current model alias
+ * @return array $fields Unaliased fields where applicable
+ */
+ protected function _removeAliases(array $fields, string $model): array
+ {
+ $result = [];
+ foreach ($fields as $field => $sort) {
+ if (strpos($field, '.') === false) {
+ $result[$field] = $sort;
+ continue;
+ }
+
+ [$alias, $currentField] = explode('.', $field);
+
+ if ($alias === $model) {
+ $result[$currentField] = $sort;
+ continue;
+ }
+
+ $result[$field] = $sort;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Prefixes the field with the table alias if possible.
+ *
+ * @param \Cake\Datasource\RepositoryInterface $object Repository object.
+ * @param array $order Order array.
+ * @param bool $allowed Whether or not the field was allowed.
+ * @return array Final order array.
+ */
+ protected function _prefix(RepositoryInterface $object, array $order, bool $allowed = false): array
+ {
+ $tableAlias = $object->getAlias();
+ $tableOrder = [];
+ foreach ($order as $key => $value) {
+ if (is_numeric($key)) {
+ $tableOrder[] = $value;
+ continue;
+ }
+ $field = $key;
+ $alias = $tableAlias;
+
+ if (strpos($key, '.') !== false) {
+ [$alias, $field] = explode('.', $key);
+ }
+ $correctAlias = ($tableAlias === $alias);
+
+ if ($correctAlias && $allowed) {
+ // Disambiguate fields in schema. As id is quite common.
+ if ($object->hasField($field)) {
+ $field = $alias . '.' . $field;
+ }
+ $tableOrder[$field] = $value;
+ } elseif ($correctAlias && $object->hasField($field)) {
+ $tableOrder[$tableAlias . '.' . $field] = $value;
+ } elseif (!$correctAlias && $allowed) {
+ $tableOrder[$alias . '.' . $field] = $value;
+ }
+ }
+
+ return $tableOrder;
+ }
+
+ /**
+ * Check the limit parameter and ensure it's within the maxLimit bounds.
+ *
+ * @param array $options An array of options with a limit key to be checked.
+ * @return array An array of options for pagination.
+ */
+ public function checkLimit(array $options): array
+ {
+ $options['limit'] = (int)$options['limit'];
+ if (empty($options['limit']) || $options['limit'] < 1) {
+ $options['limit'] = 1;
+ }
+ $options['limit'] = max(min($options['limit'], $options['maxLimit']), 1);
+
+ return $options;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/PaginatorInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/PaginatorInterface.php
new file mode 100644
index 000000000..c2bff1a17
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/PaginatorInterface.php
@@ -0,0 +1,41 @@
+_key = $key;
+
+ if (!is_string($config) && !($config instanceof CacheInterface)) {
+ throw new RuntimeException('Cache configs must be strings or \Psr\SimpleCache\CacheInterface instances.');
+ }
+ $this->_config = $config;
+ }
+
+ /**
+ * Load the cached results from the cache or run the query.
+ *
+ * @param object $query The query the cache read is for.
+ * @return mixed|null Either the cached results or null.
+ */
+ public function fetch(object $query)
+ {
+ $key = $this->_resolveKey($query);
+ $storage = $this->_resolveCacher();
+ $result = $storage->get($key);
+ if (empty($result)) {
+ return null;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Store the result set into the cache.
+ *
+ * @param object $query The query the cache read is for.
+ * @param \Traversable $results The result set to store.
+ * @return bool True if the data was successfully cached, false on failure
+ */
+ public function store(object $query, Traversable $results): bool
+ {
+ $key = $this->_resolveKey($query);
+ $storage = $this->_resolveCacher();
+
+ return $storage->set($key, $results);
+ }
+
+ /**
+ * Get/generate the cache key.
+ *
+ * @param object $query The query to generate a key for.
+ * @return string
+ * @throws \RuntimeException
+ */
+ protected function _resolveKey(object $query): string
+ {
+ if (is_string($this->_key)) {
+ return $this->_key;
+ }
+ $func = $this->_key;
+ $key = $func($query);
+ if (!is_string($key)) {
+ $msg = sprintf('Cache key functions must return a string. Got %s.', var_export($key, true));
+ throw new RuntimeException($msg);
+ }
+
+ return $key;
+ }
+
+ /**
+ * Get the cache engine.
+ *
+ * @return \Psr\SimpleCache\CacheInterface
+ */
+ protected function _resolveCacher()
+ {
+ if (is_string($this->_config)) {
+ return Cache::pool($this->_config);
+ }
+
+ return $this->_config;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php
new file mode 100644
index 000000000..9b3fc8f62
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/QueryInterface.php
@@ -0,0 +1,405 @@
+ value array representing a single aliased field
+ * that can be passed directly to the select() method.
+ * The key will contain the alias and the value the actual field name.
+ *
+ * If the field is already aliased, then it will not be changed.
+ * If no $alias is passed, the default table for this query will be used.
+ *
+ * @param string $field The field to alias
+ * @param string|null $alias the alias used to prefix the field
+ * @return array
+ */
+ public function aliasField(string $field, ?string $alias = null): array;
+
+ /**
+ * Runs `aliasField()` for each field in the provided list and returns
+ * the result under a single array.
+ *
+ * @param array $fields The fields to alias
+ * @param string|null $defaultAlias The default alias
+ * @return string[]
+ */
+ public function aliasFields(array $fields, ?string $defaultAlias = null): array;
+
+ /**
+ * Fetch the results for this query.
+ *
+ * Will return either the results set through setResult(), or execute this query
+ * and return the ResultSetDecorator object ready for streaming of results.
+ *
+ * ResultSetDecorator is a traversable object that implements the methods found
+ * on Cake\Collection\Collection.
+ *
+ * @return \Cake\Datasource\ResultSetInterface
+ */
+ public function all(): ResultSetInterface;
+
+ /**
+ * Populates or adds parts to current query clauses using an array.
+ * This is handy for passing all query clauses at once. The option array accepts:
+ *
+ * - fields: Maps to the select method
+ * - conditions: Maps to the where method
+ * - limit: Maps to the limit method
+ * - order: Maps to the order method
+ * - offset: Maps to the offset method
+ * - group: Maps to the group method
+ * - having: Maps to the having method
+ * - contain: Maps to the contain options for eager loading
+ * - join: Maps to the join method
+ * - page: Maps to the page method
+ *
+ * ### Example:
+ *
+ * ```
+ * $query->applyOptions([
+ * 'fields' => ['id', 'name'],
+ * 'conditions' => [
+ * 'created >=' => '2013-01-01'
+ * ],
+ * 'limit' => 10
+ * ]);
+ * ```
+ *
+ * Is equivalent to:
+ *
+ * ```
+ * $query
+ * ->select(['id', 'name'])
+ * ->where(['created >=' => '2013-01-01'])
+ * ->limit(10)
+ * ```
+ *
+ * @param array $options list of query clauses to apply new parts to.
+ * @return $this
+ */
+ public function applyOptions(array $options);
+
+ /**
+ * Apply custom finds to against an existing query object.
+ *
+ * Allows custom find methods to be combined and applied to each other.
+ *
+ * ```
+ * $repository->find('all')->find('recent');
+ * ```
+ *
+ * The above is an example of stacking multiple finder methods onto
+ * a single query.
+ *
+ * @param string $finder The finder method to use.
+ * @param array $options The options for the finder.
+ * @return static Returns a modified query.
+ */
+ public function find(string $finder, array $options = []);
+
+ /**
+ * Returns the first result out of executing this query, if the query has not been
+ * executed before, it will set the limit clause to 1 for performance reasons.
+ *
+ * ### Example:
+ *
+ * ```
+ * $singleUser = $query->select(['id', 'username'])->first();
+ * ```
+ *
+ * @return \Cake\Datasource\EntityInterface|array|null the first result from the ResultSet
+ */
+ public function first();
+
+ /**
+ * Returns the total amount of results for the query.
+ *
+ * @return int
+ */
+ public function count(): int;
+
+ /**
+ * Sets the number of records that should be retrieved from database,
+ * accepts an integer or an expression object that evaluates to an integer.
+ * In some databases, this operation might not be supported or will require
+ * the query to be transformed in order to limit the result set size.
+ *
+ * ### Examples
+ *
+ * ```
+ * $query->limit(10) // generates LIMIT 10
+ * $query->limit($query->newExpr()->add(['1 + 1'])); // LIMIT (1 + 1)
+ * ```
+ *
+ * @param int|\Cake\Database\ExpressionInterface|null $num number of records to be returned
+ * @return $this
+ */
+ public function limit($num);
+
+ /**
+ * Sets the number of records that should be skipped from the original result set
+ * This is commonly used for paginating large results. Accepts an integer or an
+ * expression object that evaluates to an integer.
+ *
+ * In some databases, this operation might not be supported or will require
+ * the query to be transformed in order to limit the result set size.
+ *
+ * ### Examples
+ *
+ * ```
+ * $query->offset(10) // generates OFFSET 10
+ * $query->offset($query->newExpr()->add(['1 + 1'])); // OFFSET (1 + 1)
+ * ```
+ *
+ * @param int|\Cake\Database\ExpressionInterface|null $num number of records to be skipped
+ * @return $this
+ */
+ public function offset($num);
+
+ /**
+ * Adds a single or multiple fields to be used in the ORDER clause for this query.
+ * Fields can be passed as an array of strings, array of expression
+ * objects, a single expression or a single string.
+ *
+ * If an array is passed, keys will be used as the field itself and the value will
+ * represent the order in which such field should be ordered. When called multiple
+ * times with the same fields as key, the last order definition will prevail over
+ * the others.
+ *
+ * By default this function will append any passed argument to the list of fields
+ * to be selected, unless the second argument is set to true.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $query->order(['title' => 'DESC', 'author_id' => 'ASC']);
+ * ```
+ *
+ * Produces:
+ *
+ * `ORDER BY title DESC, author_id ASC`
+ *
+ * ```
+ * $query
+ * ->order(['title' => $query->newExpr('DESC NULLS FIRST')])
+ * ->order('author_id');
+ * ```
+ *
+ * Will generate:
+ *
+ * `ORDER BY title DESC NULLS FIRST, author_id`
+ *
+ * ```
+ * $expression = $query->newExpr()->add(['id % 2 = 0']);
+ * $query->order($expression)->order(['title' => 'ASC']);
+ * ```
+ *
+ * Will become:
+ *
+ * `ORDER BY (id %2 = 0), title ASC`
+ *
+ * If you need to set complex expressions as order conditions, you
+ * should use `orderAsc()` or `orderDesc()`.
+ *
+ * @param array|\Cake\Database\ExpressionInterface|\Closure|string $fields fields to be added to the list
+ * @param bool $overwrite whether to reset order with field list or not
+ * @return $this
+ */
+ public function order($fields, $overwrite = false);
+
+ /**
+ * Set the page of results you want.
+ *
+ * This method provides an easier to use interface to set the limit + offset
+ * in the record set you want as results. If empty the limit will default to
+ * the existing limit clause, and if that too is empty, then `25` will be used.
+ *
+ * Pages must start at 1.
+ *
+ * @param int $num The page number you want.
+ * @param int|null $limit The number of rows you want in the page. If null
+ * the current limit clause will be used.
+ * @return $this
+ * @throws \InvalidArgumentException If page number < 1.
+ */
+ public function page(int $num, ?int $limit = null);
+
+ /**
+ * Returns an array representation of the results after executing the query.
+ *
+ * @return array
+ */
+ public function toArray(): array;
+
+ /**
+ * Set the default Table object that will be used by this query
+ * and form the `FROM` clause.
+ *
+ * @param \Cake\Datasource\RepositoryInterface $repository The default repository object to use
+ * @return $this
+ */
+ public function repository(RepositoryInterface $repository);
+
+ /**
+ * Returns the default repository object that will be used by this query,
+ * that is, the repository that will appear in the from clause.
+ *
+ * @return \Cake\Datasource\RepositoryInterface|null $repository The default repository object to use
+ */
+ public function getRepository(): ?RepositoryInterface;
+
+ /**
+ * Adds a condition or set of conditions to be used in the WHERE clause for this
+ * query. Conditions can be expressed as an array of fields as keys with
+ * comparison operators in it, the values for the array will be used for comparing
+ * the field to such literal. Finally, conditions can be expressed as a single
+ * string or an array of strings.
+ *
+ * When using arrays, each entry will be joined to the rest of the conditions using
+ * an AND operator. Consecutive calls to this function will also join the new
+ * conditions specified using the AND operator. Additionally, values can be
+ * expressed using expression objects which can include other query objects.
+ *
+ * Any conditions created with this methods can be used with any SELECT, UPDATE
+ * and DELETE type of queries.
+ *
+ * ### Conditions using operators:
+ *
+ * ```
+ * $query->where([
+ * 'posted >=' => new DateTime('3 days ago'),
+ * 'title LIKE' => 'Hello W%',
+ * 'author_id' => 1,
+ * ], ['posted' => 'datetime']);
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE posted >= 2012-01-27 AND title LIKE 'Hello W%' AND author_id = 1`
+ *
+ * Second parameter is used to specify what type is expected for each passed
+ * key. Valid types can be used from the mapped with Database\Type class.
+ *
+ * ### Nesting conditions with conjunctions:
+ *
+ * ```
+ * $query->where([
+ * 'author_id !=' => 1,
+ * 'OR' => ['published' => true, 'posted <' => new DateTime('now')],
+ * 'NOT' => ['title' => 'Hello']
+ * ], ['published' => boolean, 'posted' => 'datetime']
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE author_id = 1 AND (published = 1 OR posted < '2012-02-01') AND NOT (title = 'Hello')`
+ *
+ * You can nest conditions using conjunctions as much as you like. Sometimes, you
+ * may want to define 2 different options for the same key, in that case, you can
+ * wrap each condition inside a new array:
+ *
+ * `$query->where(['OR' => [['published' => false], ['published' => true]])`
+ *
+ * Keep in mind that every time you call where() with the third param set to false
+ * (default), it will join the passed conditions to the previous stored list using
+ * the AND operator. Also, using the same array key twice in consecutive calls to
+ * this method will not override the previous value.
+ *
+ * ### Using expressions objects:
+ *
+ * ```
+ * $exp = $query->newExpr()->add(['id !=' => 100, 'author_id' != 1])->tieWith('OR');
+ * $query->where(['published' => true], ['published' => 'boolean'])->where($exp);
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE (id != 100 OR author_id != 1) AND published = 1`
+ *
+ * Other Query objects that be used as conditions for any field.
+ *
+ * ### Adding conditions in multiple steps:
+ *
+ * You can use callable functions to construct complex expressions, functions
+ * receive as first argument a new QueryExpression object and this query instance
+ * as second argument. Functions must return an expression object, that will be
+ * added the list of conditions for the query using the AND operator.
+ *
+ * ```
+ * $query
+ * ->where(['title !=' => 'Hello World'])
+ * ->where(function ($exp, $query) {
+ * $or = $exp->or(['id' => 1]);
+ * $and = $exp->and(['id >' => 2, 'id <' => 10]);
+ * return $or->add($and);
+ * });
+ * ```
+ *
+ * * The previous example produces:
+ *
+ * `WHERE title != 'Hello World' AND (id = 1 OR (id > 2 AND id < 10))`
+ *
+ * ### Conditions as strings:
+ *
+ * ```
+ * $query->where(['articles.author_id = authors.id', 'modified IS NULL']);
+ * ```
+ *
+ * The previous example produces:
+ *
+ * `WHERE articles.author_id = authors.id AND modified IS NULL`
+ *
+ * Please note that when using the array notation or the expression objects, all
+ * values will be correctly quoted and transformed to the correspondent database
+ * data type automatically for you, thus securing your application from SQL injections.
+ * If you use string conditions make sure that your values are correctly quoted.
+ * The safest thing you can do is to never use string conditions.
+ *
+ * @param string|array|\Closure|null $conditions The conditions to filter on.
+ * @param array $types associative array of type names used to bind values to query
+ * @param bool $overwrite whether to reset conditions with passed list or not
+ * @return $this
+ */
+ public function where($conditions = null, array $types = [], bool $overwrite = false);
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/QueryTrait.php b/app/vendor/cakephp/cakephp/src/Datasource/QueryTrait.php
new file mode 100644
index 000000000..3201f24db
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/QueryTrait.php
@@ -0,0 +1,618 @@
+_repository = $repository;
+
+ return $this;
+ }
+
+ /**
+ * Returns the default table object that will be used by this query,
+ * that is, the table that will appear in the from clause.
+ *
+ * @return \Cake\Datasource\RepositoryInterface
+ */
+ public function getRepository(): RepositoryInterface
+ {
+ return $this->_repository;
+ }
+
+ /**
+ * Set the result set for a query.
+ *
+ * Setting the resultset of a query will make execute() a no-op. Instead
+ * of executing the SQL query and fetching results, the ResultSet provided to this
+ * method will be returned.
+ *
+ * This method is most useful when combined with results stored in a persistent cache.
+ *
+ * @param iterable $results The results this query should return.
+ * @return $this
+ */
+ public function setResult(iterable $results)
+ {
+ $this->_results = $results;
+
+ return $this;
+ }
+
+ /**
+ * Executes this query and returns a results iterator. This function is required
+ * for implementing the IteratorAggregate interface and allows the query to be
+ * iterated without having to call execute() manually, thus making it look like
+ * a result set instead of the query itself.
+ *
+ * @return \Cake\Datasource\ResultSetInterface
+ * @psalm-suppress ImplementedReturnTypeMismatch
+ */
+ public function getIterator()
+ {
+ return $this->all();
+ }
+
+ /**
+ * Enable result caching for this query.
+ *
+ * If a query has caching enabled, it will do the following when executed:
+ *
+ * - Check the cache for $key. If there are results no SQL will be executed.
+ * Instead the cached results will be returned.
+ * - When the cached data is stale/missing the result set will be cached as the query
+ * is executed.
+ *
+ * ### Usage
+ *
+ * ```
+ * // Simple string key + config
+ * $query->cache('my_key', 'db_results');
+ *
+ * // Function to generate key.
+ * $query->cache(function ($q) {
+ * $key = serialize($q->clause('select'));
+ * $key .= serialize($q->clause('where'));
+ * return md5($key);
+ * });
+ *
+ * // Using a pre-built cache engine.
+ * $query->cache('my_key', $engine);
+ *
+ * // Disable caching
+ * $query->cache(false);
+ * ```
+ *
+ * @param \Closure|string|false $key Either the cache key or a function to generate the cache key.
+ * When using a function, this query instance will be supplied as an argument.
+ * @param string|\Psr\SimpleCache\CacheInterface $config Either the name of the cache config to use, or
+ * a cache engine instance.
+ * @return $this
+ */
+ public function cache($key, $config = 'default')
+ {
+ if ($key === false) {
+ $this->_cache = null;
+
+ return $this;
+ }
+ $this->_cache = new QueryCacher($key, $config);
+
+ return $this;
+ }
+
+ /**
+ * Returns the current configured query `_eagerLoaded` value
+ *
+ * @return bool
+ */
+ public function isEagerLoaded(): bool
+ {
+ return $this->_eagerLoaded;
+ }
+
+ /**
+ * Sets the query instance to be an eager loaded query. If no argument is
+ * passed, the current configured query `_eagerLoaded` value is returned.
+ *
+ * @param bool $value Whether or not to eager load.
+ * @return $this
+ */
+ public function eagerLoaded(bool $value)
+ {
+ $this->_eagerLoaded = $value;
+
+ return $this;
+ }
+
+ /**
+ * Returns a key => value array representing a single aliased field
+ * that can be passed directly to the select() method.
+ * The key will contain the alias and the value the actual field name.
+ *
+ * If the field is already aliased, then it will not be changed.
+ * If no $alias is passed, the default table for this query will be used.
+ *
+ * @param string $field The field to alias
+ * @param string|null $alias the alias used to prefix the field
+ * @return array
+ */
+ public function aliasField(string $field, ?string $alias = null): array
+ {
+ $namespaced = strpos($field, '.') !== false;
+ $aliasedField = $field;
+
+ if ($namespaced) {
+ [$alias, $field] = explode('.', $field);
+ }
+
+ if (!$alias) {
+ $alias = $this->getRepository()->getAlias();
+ }
+
+ $key = sprintf('%s__%s', $alias, $field);
+ if (!$namespaced) {
+ $aliasedField = $alias . '.' . $field;
+ }
+
+ return [$key => $aliasedField];
+ }
+
+ /**
+ * Runs `aliasField()` for each field in the provided list and returns
+ * the result under a single array.
+ *
+ * @param array $fields The fields to alias
+ * @param string|null $defaultAlias The default alias
+ * @return string[]
+ */
+ public function aliasFields(array $fields, ?string $defaultAlias = null): array
+ {
+ $aliased = [];
+ foreach ($fields as $alias => $field) {
+ if (is_numeric($alias) && is_string($field)) {
+ $aliased += $this->aliasField($field, $defaultAlias);
+ continue;
+ }
+ $aliased[$alias] = $field;
+ }
+
+ return $aliased;
+ }
+
+ /**
+ * Fetch the results for this query.
+ *
+ * Will return either the results set through setResult(), or execute this query
+ * and return the ResultSetDecorator object ready for streaming of results.
+ *
+ * ResultSetDecorator is a traversable object that implements the methods found
+ * on Cake\Collection\Collection.
+ *
+ * @return \Cake\Datasource\ResultSetInterface
+ */
+ public function all(): ResultSetInterface
+ {
+ if ($this->_results !== null) {
+ return $this->_results;
+ }
+
+ $results = null;
+ if ($this->_cache) {
+ $results = $this->_cache->fetch($this);
+ }
+ if ($results === null) {
+ $results = $this->_decorateResults($this->_execute());
+ if ($this->_cache) {
+ $this->_cache->store($this, $results);
+ }
+ }
+ $this->_results = $results;
+
+ return $this->_results;
+ }
+
+ /**
+ * Returns an array representation of the results after executing the query.
+ *
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->all()->toArray();
+ }
+
+ /**
+ * Register a new MapReduce routine to be executed on top of the database results
+ * Both the mapper and caller callable should be invokable objects.
+ *
+ * The MapReduce routing will only be run when the query is executed and the first
+ * result is attempted to be fetched.
+ *
+ * If the third argument is set to true, it will erase previous map reducers
+ * and replace it with the arguments passed.
+ *
+ * @param callable|null $mapper The mapper callable.
+ * @param callable|null $reducer The reducing function.
+ * @param bool $overwrite Set to true to overwrite existing map + reduce functions.
+ * @return $this
+ * @see \Cake\Collection\Iterator\MapReduce for details on how to use emit data to the map reducer.
+ */
+ public function mapReduce(?callable $mapper = null, ?callable $reducer = null, bool $overwrite = false)
+ {
+ if ($overwrite) {
+ $this->_mapReduce = [];
+ }
+ if ($mapper === null) {
+ if (!$overwrite) {
+ throw new InvalidArgumentException('$mapper can be null only when $overwrite is true.');
+ }
+
+ return $this;
+ }
+ $this->_mapReduce[] = compact('mapper', 'reducer');
+
+ return $this;
+ }
+
+ /**
+ * Returns the list of previously registered map reduce routines.
+ *
+ * @return array
+ */
+ public function getMapReducers(): array
+ {
+ return $this->_mapReduce;
+ }
+
+ /**
+ * Registers a new formatter callback function that is to be executed when trying
+ * to fetch the results from the database.
+ *
+ * If the second argument is set to true, it will erase previous formatters
+ * and replace them with the passed first argument.
+ *
+ * Callbacks are required to return an iterator object, which will be used as
+ * the return value for this query's result. Formatter functions are applied
+ * after all the `MapReduce` routines for this query have been executed.
+ *
+ * Formatting callbacks will receive two arguments, the first one being an object
+ * implementing `\Cake\Collection\CollectionInterface`, that can be traversed and
+ * modified at will. The second one being the query instance on which the formatter
+ * callback is being applied.
+ *
+ * Usually the query instance received by the formatter callback is the same query
+ * instance on which the callback was attached to, except for in a joined
+ * association, in that case the callback will be invoked on the association source
+ * side query, and it will receive that query instance instead of the one on which
+ * the callback was originally attached to - see the examples below!
+ *
+ * ### Examples:
+ *
+ * Return all results from the table indexed by id:
+ *
+ * ```
+ * $query->select(['id', 'name'])->formatResults(function ($results) {
+ * return $results->indexBy('id');
+ * });
+ * ```
+ *
+ * Add a new column to the ResultSet:
+ *
+ * ```
+ * $query->select(['name', 'birth_date'])->formatResults(function ($results) {
+ * return $results->map(function ($row) {
+ * $row['age'] = $row['birth_date']->diff(new DateTime)->y;
+ *
+ * return $row;
+ * });
+ * });
+ * ```
+ *
+ * Add a new column to the results with respect to the query's hydration configuration:
+ *
+ * ```
+ * $query->formatResults(function ($results, $query) {
+ * return $results->map(function ($row) use ($query) {
+ * $data = [
+ * 'bar' => 'baz',
+ * ];
+ *
+ * if ($query->isHydrationEnabled()) {
+ * $row['foo'] = new Foo($data)
+ * } else {
+ * $row['foo'] = $data;
+ * }
+ *
+ * return $row;
+ * });
+ * });
+ * ```
+ *
+ * Retaining access to the association target query instance of joined associations,
+ * by inheriting the contain callback's query argument:
+ *
+ * ```
+ * // Assuming a `Articles belongsTo Authors` association that uses the join strategy
+ *
+ * $articlesQuery->contain('Authors', function ($authorsQuery) {
+ * return $authorsQuery->formatResults(function ($results, $query) use ($authorsQuery) {
+ * // Here `$authorsQuery` will always be the instance
+ * // where the callback was attached to.
+ *
+ * // The instance passed to the callback in the second
+ * // argument (`$query`), will be the one where the
+ * // callback is actually being applied to, in this
+ * // example that would be `$articlesQuery`.
+ *
+ * // ...
+ *
+ * return $results;
+ * });
+ * });
+ * ```
+ *
+ * @param callable|null $formatter The formatting callable.
+ * @param int|true $mode Whether or not to overwrite, append or prepend the formatter.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function formatResults(?callable $formatter = null, $mode = self::APPEND)
+ {
+ if ($mode === self::OVERWRITE) {
+ $this->_formatters = [];
+ }
+ if ($formatter === null) {
+ if ($mode !== self::OVERWRITE) {
+ throw new InvalidArgumentException('$formatter can be null only when $mode is overwrite.');
+ }
+
+ return $this;
+ }
+
+ if ($mode === self::PREPEND) {
+ array_unshift($this->_formatters, $formatter);
+
+ return $this;
+ }
+
+ $this->_formatters[] = $formatter;
+
+ return $this;
+ }
+
+ /**
+ * Returns the list of previously registered format routines.
+ *
+ * @return callable[]
+ */
+ public function getResultFormatters(): array
+ {
+ return $this->_formatters;
+ }
+
+ /**
+ * Returns the first result out of executing this query, if the query has not been
+ * executed before, it will set the limit clause to 1 for performance reasons.
+ *
+ * ### Example:
+ *
+ * ```
+ * $singleUser = $query->select(['id', 'username'])->first();
+ * ```
+ *
+ * @return \Cake\Datasource\EntityInterface|array|null The first result from the ResultSet.
+ */
+ public function first()
+ {
+ if ($this->_dirty) {
+ $this->limit(1);
+ }
+
+ return $this->all()->first();
+ }
+
+ /**
+ * Get the first result from the executing query or raise an exception.
+ *
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record.
+ * @return \Cake\Datasource\EntityInterface|array The first result from the ResultSet.
+ */
+ public function firstOrFail()
+ {
+ $entity = $this->first();
+ if (!$entity) {
+ $table = $this->getRepository();
+ throw new RecordNotFoundException(sprintf(
+ 'Record not found in table "%s"',
+ $table->getTable()
+ ));
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Returns an array with the custom options that were applied to this query
+ * and that were not already processed by another method in this class.
+ *
+ * ### Example:
+ *
+ * ```
+ * $query->applyOptions(['doABarrelRoll' => true, 'fields' => ['id', 'name']);
+ * $query->getOptions(); // Returns ['doABarrelRoll' => true]
+ * ```
+ *
+ * @see \Cake\Datasource\QueryInterface::applyOptions() to read about the options that will
+ * be processed by this class and not returned by this function
+ * @return array
+ */
+ public function getOptions(): array
+ {
+ return $this->_options;
+ }
+
+ /**
+ * Enables calling methods from the result set as if they were from this class
+ *
+ * @param string $method the method to call
+ * @param array $arguments list of arguments for the method to call
+ * @return mixed
+ * @throws \BadMethodCallException if no such method exists in result set
+ */
+ public function __call(string $method, array $arguments)
+ {
+ $resultSetClass = $this->_decoratorClass();
+ if (in_array($method, get_class_methods($resultSetClass), true)) {
+ $results = $this->all();
+
+ return $results->$method(...$arguments);
+ }
+ throw new BadMethodCallException(
+ sprintf('Unknown method "%s"', $method)
+ );
+ }
+
+ /**
+ * Populates or adds parts to current query clauses using an array.
+ * This is handy for passing all query clauses at once.
+ *
+ * @param array $options the options to be applied
+ * @return $this
+ */
+ abstract public function applyOptions(array $options);
+
+ /**
+ * Executes this query and returns a traversable object containing the results
+ *
+ * @return \Cake\Datasource\ResultSetInterface
+ */
+ abstract protected function _execute(): ResultSetInterface;
+
+ /**
+ * Decorates the results iterator with MapReduce routines and formatters
+ *
+ * @param \Traversable $result Original results
+ * @return \Cake\Datasource\ResultSetInterface
+ */
+ protected function _decorateResults(Traversable $result): ResultSetInterface
+ {
+ $decorator = $this->_decoratorClass();
+ foreach ($this->_mapReduce as $functions) {
+ $result = new MapReduce($result, $functions['mapper'], $functions['reducer']);
+ }
+
+ if (!empty($this->_mapReduce)) {
+ $result = new $decorator($result);
+ }
+
+ foreach ($this->_formatters as $formatter) {
+ $result = $formatter($result, $this);
+ }
+
+ if (!empty($this->_formatters) && !($result instanceof $decorator)) {
+ $result = new $decorator($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the name of the class to be used for decorating results
+ *
+ * @return string
+ * @psalm-return class-string<\Cake\Datasource\ResultSetInterface>
+ */
+ protected function _decoratorClass(): string
+ {
+ return ResultSetDecorator::class;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/README.md b/app/vendor/cakephp/cakephp/src/Datasource/README.md
new file mode 100644
index 000000000..38626ea63
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/README.md
@@ -0,0 +1,82 @@
+[](https://packagist.org/packages/cakephp/datasource)
+[](LICENSE.txt)
+
+# CakePHP Datasource Library
+
+This library contains interfaces for implementing Repositories and Entities using any data source,
+a class for managing connections to datasources and traits to help you quickly implement the
+interfaces provided by this package.
+
+## Repositories
+
+A repository is a class capable of interfacing with a data source using operations such as
+`find`, `save` and `delete` by using intermediate query objects for expressing commands to
+the data store and returning Entities as the single result unit of such system.
+
+In the case of a Relational database, a Repository would be a `Table`, which can be return single
+or multiple `Entity` objects by using a `Query`.
+
+This library exposes the following interfaces for creating a system that implements the
+repository pattern and is compatible with the CakePHP framework:
+
+* `RepositoryInterface` - Describes the methods for a base repository class.
+* `EntityInterface` - Describes the methods for a single result object.
+* `ResultSetInterface` - Represents the idea of a collection of Entities as a result of a query.
+
+Additionally, this package provides a few traits and classes you can use in your own implementations:
+
+* `EntityTrait` - Contains the default implementation for the `EntityInterface`.
+* `QueryTrait` - Exposes the methods for creating a query object capable of returning decoratable collections.
+* `ResultSetDecorator` - Decorates any traversable object, so it complies with `ResultSetInterface`.
+
+
+## Connections
+
+This library contains a couple of utility classes meant to create and manage
+connection objects. Connections are typically used in repositories for
+interfacing with the actual data source system.
+
+The `ConnectionManager` class acts as a registry to access database connections
+your application has. It provides a place that other objects can get references
+to existing connections. Creating connections with the `ConnectionManager` is
+easy:
+
+```php
+use Cake\Datasource\ConnectionManager;
+
+ConnectionManager::config('connection-one', [
+ 'className' => 'MyApp\Connections\CustomConnection',
+ 'param1' => 'value',
+ 'param2' => 'another value'
+]);
+
+ConnectionManager::config('connection-two', [
+ 'className' => 'MyApp\Connections\CustomConnection',
+ 'param1' => 'different value',
+ 'param2' => 'another value'
+]);
+```
+
+When requested, the `ConnectionManager` will instantiate
+`MyApp\Connections\CustomConnection` by passing `param1` and `param2` inside an
+array as the first argument of the constructor.
+
+Once configured connections can be fetched using `ConnectionManager::get()`.
+This method will construct and load a connection if it has not been built
+before, or return the existing known connection:
+
+```php
+use Cake\Datasource\ConnectionManager;
+$conn = ConnectionManager::get('master');
+```
+
+It is also possible to store connection objects by passing the instance directly to the manager:
+
+```php
+use Cake\Datasource\ConnectionManager;
+$conn = ConnectionManager::config('other', $connectionInstance);
+```
+
+## Documentation
+
+Please make sure you check the [official API documentation](https://api.cakephp.org/4.x/namespace-Cake.Datasource.html)
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/RepositoryInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/RepositoryInterface.php
new file mode 100644
index 000000000..b8ffbeb2c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/RepositoryInterface.php
@@ -0,0 +1,252 @@
+get($id);
+ *
+ * $article = $articles->get($id, ['contain' => ['Comments]]);
+ * ```
+ *
+ * @param mixed $primaryKey primary key value to find
+ * @param array $options options accepted by `Table::find()`
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id
+ * could not be found
+ * @return \Cake\Datasource\EntityInterface
+ * @see \Cake\Datasource\RepositoryInterface::find()
+ */
+ public function get($primaryKey, array $options = []): EntityInterface;
+
+ /**
+ * Creates a new Query instance for this repository
+ *
+ * @return \Cake\Datasource\QueryInterface
+ */
+ public function query();
+
+ /**
+ * Update all matching records.
+ *
+ * Sets the $fields to the provided values based on $conditions.
+ * This method will *not* trigger beforeSave/afterSave events. If you need those
+ * first load a collection of records and update them.
+ *
+ * @param string|array|\Closure|\Cake\Database\Expression\QueryExpression $fields A hash of field => new value.
+ * @param mixed $conditions Conditions to be used, accepts anything Query::where()
+ * can take.
+ * @return int Count Returns the affected rows.
+ */
+ public function updateAll($fields, $conditions): int;
+
+ /**
+ * Deletes all records matching the provided conditions.
+ *
+ * This method will *not* trigger beforeDelete/afterDelete events. If you
+ * need those first load a collection of records and delete them.
+ *
+ * This method will *not* execute on associations' `cascade` attribute. You should
+ * use database foreign keys + ON CASCADE rules if you need cascading deletes combined
+ * with this method.
+ *
+ * @param mixed $conditions Conditions to be used, accepts anything Query::where()
+ * can take.
+ * @return int Returns the number of affected rows.
+ * @see \Cake\Datasource\RepositoryInterface::delete()
+ */
+ public function deleteAll($conditions): int;
+
+ /**
+ * Returns true if there is any record in this repository matching the specified
+ * conditions.
+ *
+ * @param array $conditions list of conditions to pass to the query
+ * @return bool
+ */
+ public function exists($conditions): bool;
+
+ /**
+ * Persists an entity based on the fields that are marked as dirty and
+ * returns the same entity after a successful save or false in case
+ * of any error.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
+ * @param array|\ArrayAccess $options The options to use when saving.
+ * @return \Cake\Datasource\EntityInterface|false
+ */
+ public function save(EntityInterface $entity, $options = []);
+
+ /**
+ * Delete a single entity.
+ *
+ * Deletes an entity and possibly related associations from the database
+ * based on the 'dependent' option used when defining the association.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to remove.
+ * @param array|\ArrayAccess $options The options for the delete.
+ * @return bool success
+ */
+ public function delete(EntityInterface $entity, $options = []): bool;
+
+ /**
+ * This creates a new entity object.
+ *
+ * Careful: This does not trigger any field validation.
+ * This entity can be persisted without validation error as empty record.
+ * Always patch in required fields before saving.
+ *
+ * @return \Cake\Datasource\EntityInterface
+ */
+ public function newEmptyEntity(): EntityInterface;
+
+ /**
+ * Create a new entity + associated entities from an array.
+ *
+ * This is most useful when hydrating request data back into entities.
+ * For example, in your controller code:
+ *
+ * ```
+ * $article = $this->Articles->newEntity($this->request->getData());
+ * ```
+ *
+ * The hydrated entity will correctly do an insert/update based
+ * on the primary key data existing in the database when the entity
+ * is saved. Until the entity is saved, it will be a detached record.
+ *
+ * @param array $data The data to build an entity with.
+ * @param array $options A list of options for the object hydration.
+ * @return \Cake\Datasource\EntityInterface
+ */
+ public function newEntity(array $data, array $options = []): EntityInterface;
+
+ /**
+ * Create a list of entities + associated entities from an array.
+ *
+ * This is most useful when hydrating request data back into entities.
+ * For example, in your controller code:
+ *
+ * ```
+ * $articles = $this->Articles->newEntities($this->request->getData());
+ * ```
+ *
+ * The hydrated entities can then be iterated and saved.
+ *
+ * @param array $data The data to build an entity with.
+ * @param array $options A list of options for the objects hydration.
+ * @return \Cake\Datasource\EntityInterface[] An array of hydrated records.
+ */
+ public function newEntities(array $data, array $options = []): array;
+
+ /**
+ * Merges the passed `$data` into `$entity` respecting the accessible
+ * fields configured on the entity. Returns the same entity after being
+ * altered.
+ *
+ * This is most useful when editing an existing entity using request data:
+ *
+ * ```
+ * $article = $this->Articles->patchEntity($article, $this->request->getData());
+ * ```
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
+ * data merged in
+ * @param array $data key value list of fields to be merged into the entity
+ * @param array $options A list of options for the object hydration.
+ * @return \Cake\Datasource\EntityInterface
+ */
+ public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface;
+
+ /**
+ * Merges each of the elements passed in `$data` into the entities
+ * found in `$entities` respecting the accessible fields configured on the entities.
+ * Merging is done by matching the primary key in each of the elements in `$data`
+ * and `$entities`.
+ *
+ * This is most useful when editing a list of existing entities using request data:
+ *
+ * ```
+ * $article = $this->Articles->patchEntities($articles, $this->request->getData());
+ * ```
+ *
+ * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the
+ * data merged in
+ * @param array $data list of arrays to be merged into the entities
+ * @param array $options A list of options for the objects hydration.
+ * @return \Cake\Datasource\EntityInterface[]
+ */
+ public function patchEntities(iterable $entities, array $data, array $options = []): array;
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ResultSetDecorator.php b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetDecorator.php
new file mode 100644
index 000000000..e59e63e54
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetDecorator.php
@@ -0,0 +1,46 @@
+getInnerIterator();
+ if ($iterator instanceof Countable) {
+ return $iterator->count();
+ }
+
+ return count($this->toArray());
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/ResultSetInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetInterface.php
new file mode 100644
index 000000000..a62ee77f3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/ResultSetInterface.php
@@ -0,0 +1,28 @@
+rule = $rule;
+ $this->name = $name;
+ $this->options = $options;
+ }
+
+ /**
+ * Set options for the rule invocation.
+ *
+ * Old options will be merged with the new ones.
+ *
+ * @param array $options The options to set.
+ * @return $this
+ */
+ public function setOptions(array $options)
+ {
+ $this->options = $options + $this->options;
+
+ return $this;
+ }
+
+ /**
+ * Set the rule name.
+ *
+ * Only truthy names will be set.
+ *
+ * @param string|null $name The name to set.
+ * @return $this
+ */
+ public function setName(?string $name)
+ {
+ if ($name) {
+ $this->name = $name;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Invoke the rule.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity the rule
+ * should apply to.
+ * @param array $scope The rule's scope/options.
+ * @return bool Whether or not the rule passed.
+ */
+ public function __invoke(EntityInterface $entity, array $scope): bool
+ {
+ $rule = $this->rule;
+ $pass = $rule($entity, $this->options + $scope);
+ if ($pass === true || empty($this->options['errorField'])) {
+ return $pass === true;
+ }
+
+ $message = 'invalid';
+ if (isset($this->options['message'])) {
+ $message = $this->options['message'];
+ }
+ if (is_string($pass)) {
+ $message = $pass;
+ }
+ if ($this->name) {
+ $message = [$this->name => $message];
+ } else {
+ $message = [$message];
+ }
+ $errorField = $this->options['errorField'];
+ $entity->setError($errorField, $message);
+
+ if ($entity instanceof InvalidPropertyInterface && isset($entity->{$errorField})) {
+ $invalidValue = $entity->{$errorField};
+ $entity->setInvalidField($errorField, $invalidValue);
+ }
+
+ return $pass === true;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/RulesAwareTrait.php b/app/vendor/cakephp/cakephp/src/Datasource/RulesAwareTrait.php
new file mode 100644
index 000000000..0e70f3750
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/RulesAwareTrait.php
@@ -0,0 +1,120 @@
+rulesChecker();
+ $options = $options ?: new ArrayObject();
+ $options = is_array($options) ? new ArrayObject($options) : $options;
+ $hasEvents = ($this instanceof EventDispatcherInterface);
+
+ if ($hasEvents) {
+ $event = $this->dispatchEvent(
+ 'Model.beforeRules',
+ compact('entity', 'options', 'operation')
+ );
+ if ($event->isStopped()) {
+ return $event->getResult();
+ }
+ }
+
+ $result = $rules->check($entity, $operation, $options->getArrayCopy());
+
+ if ($hasEvents) {
+ $event = $this->dispatchEvent(
+ 'Model.afterRules',
+ compact('entity', 'options', 'result', 'operation')
+ );
+
+ if ($event->isStopped()) {
+ return $event->getResult();
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the RulesChecker for this instance.
+ *
+ * A RulesChecker object is used to test an entity for validity
+ * on rules that may involve complex logic or data that
+ * needs to be fetched from relevant datasources.
+ *
+ * @see \Cake\Datasource\RulesChecker
+ * @return \Cake\Datasource\RulesChecker
+ */
+ public function rulesChecker(): RulesChecker
+ {
+ if ($this->_rulesChecker !== null) {
+ return $this->_rulesChecker;
+ }
+ /** @psalm-var class-string<\Cake\Datasource\RulesChecker> $class */
+ $class = defined('static::RULES_CLASS') ? static::RULES_CLASS : RulesChecker::class;
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $this->_rulesChecker = $this->buildRules(new $class(['repository' => $this]));
+ $this->dispatchEvent('Model.buildRules', ['rules' => $this->_rulesChecker]);
+
+ return $this->_rulesChecker;
+ }
+
+ /**
+ * Returns a RulesChecker object after modifying the one that was supplied.
+ *
+ * Subclasses should override this method in order to initialize the rules to be applied to
+ * entities saved by this instance.
+ *
+ * @param \Cake\Datasource\RulesChecker $rules The rules object to be modified.
+ * @return \Cake\Datasource\RulesChecker
+ */
+ public function buildRules(RulesChecker $rules): RulesChecker
+ {
+ return $rules;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/RulesChecker.php b/app/vendor/cakephp/cakephp/src/Datasource/RulesChecker.php
new file mode 100644
index 000000000..2d7e76225
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/RulesChecker.php
@@ -0,0 +1,330 @@
+_options = $options;
+ $this->_useI18n = function_exists('__d');
+ }
+
+ /**
+ * Adds a rule that will be applied to the entity both on create and update
+ * operations.
+ *
+ * ### Options
+ *
+ * The options array accept the following special keys:
+ *
+ * - `errorField`: The name of the entity field that will be marked as invalid
+ * if the rule does not pass.
+ * - `message`: The error message to set to `errorField` if the rule does not pass.
+ *
+ * @param callable $rule A callable function or object that will return whether
+ * the entity is valid or not.
+ * @param string|array|null $name The alias for a rule, or an array of options.
+ * @param array $options List of extra options to pass to the rule callable as
+ * second argument.
+ * @return $this
+ */
+ public function add(callable $rule, $name = null, array $options = [])
+ {
+ $this->_rules[] = $this->_addError($rule, $name, $options);
+
+ return $this;
+ }
+
+ /**
+ * Adds a rule that will be applied to the entity on create operations.
+ *
+ * ### Options
+ *
+ * The options array accept the following special keys:
+ *
+ * - `errorField`: The name of the entity field that will be marked as invalid
+ * if the rule does not pass.
+ * - `message`: The error message to set to `errorField` if the rule does not pass.
+ *
+ * @param callable $rule A callable function or object that will return whether
+ * the entity is valid or not.
+ * @param string|array|null $name The alias for a rule or an array of options.
+ * @param array $options List of extra options to pass to the rule callable as
+ * second argument.
+ * @return $this
+ */
+ public function addCreate(callable $rule, $name = null, array $options = [])
+ {
+ $this->_createRules[] = $this->_addError($rule, $name, $options);
+
+ return $this;
+ }
+
+ /**
+ * Adds a rule that will be applied to the entity on update operations.
+ *
+ * ### Options
+ *
+ * The options array accept the following special keys:
+ *
+ * - `errorField`: The name of the entity field that will be marked as invalid
+ * if the rule does not pass.
+ * - `message`: The error message to set to `errorField` if the rule does not pass.
+ *
+ * @param callable $rule A callable function or object that will return whether
+ * the entity is valid or not.
+ * @param string|array|null $name The alias for a rule, or an array of options.
+ * @param array $options List of extra options to pass to the rule callable as
+ * second argument.
+ * @return $this
+ */
+ public function addUpdate(callable $rule, $name = null, array $options = [])
+ {
+ $this->_updateRules[] = $this->_addError($rule, $name, $options);
+
+ return $this;
+ }
+
+ /**
+ * Adds a rule that will be applied to the entity on delete operations.
+ *
+ * ### Options
+ *
+ * The options array accept the following special keys:
+ *
+ * - `errorField`: The name of the entity field that will be marked as invalid
+ * if the rule does not pass.
+ * - `message`: The error message to set to `errorField` if the rule does not pass.
+ *
+ * @param callable $rule A callable function or object that will return whether
+ * the entity is valid or not.
+ * @param string|array|null $name The alias for a rule, or an array of options.
+ * @param array $options List of extra options to pass to the rule callable as
+ * second argument.
+ * @return $this
+ */
+ public function addDelete(callable $rule, $name = null, array $options = [])
+ {
+ $this->_deleteRules[] = $this->_addError($rule, $name, $options);
+
+ return $this;
+ }
+
+ /**
+ * Runs each of the rules by passing the provided entity and returns true if all
+ * of them pass. The rules to be applied are depended on the $mode parameter which
+ * can only be RulesChecker::CREATE, RulesChecker::UPDATE or RulesChecker::DELETE
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
+ * @param string $mode Either 'create, 'update' or 'delete'.
+ * @param array $options Extra options to pass to checker functions.
+ * @return bool
+ * @throws \InvalidArgumentException if an invalid mode is passed.
+ */
+ public function check(EntityInterface $entity, string $mode, array $options = []): bool
+ {
+ if ($mode === self::CREATE) {
+ return $this->checkCreate($entity, $options);
+ }
+
+ if ($mode === self::UPDATE) {
+ return $this->checkUpdate($entity, $options);
+ }
+
+ if ($mode === self::DELETE) {
+ return $this->checkDelete($entity, $options);
+ }
+
+ throw new InvalidArgumentException('Wrong checking mode: ' . $mode);
+ }
+
+ /**
+ * Runs each of the rules by passing the provided entity and returns true if all
+ * of them pass. The rules selected will be only those specified to be run on 'create'
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
+ * @param array $options Extra options to pass to checker functions.
+ * @return bool
+ */
+ public function checkCreate(EntityInterface $entity, array $options = []): bool
+ {
+ return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_createRules));
+ }
+
+ /**
+ * Runs each of the rules by passing the provided entity and returns true if all
+ * of them pass. The rules selected will be only those specified to be run on 'update'
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
+ * @param array $options Extra options to pass to checker functions.
+ * @return bool
+ */
+ public function checkUpdate(EntityInterface $entity, array $options = []): bool
+ {
+ return $this->_checkRules($entity, $options, array_merge($this->_rules, $this->_updateRules));
+ }
+
+ /**
+ * Runs each of the rules by passing the provided entity and returns true if all
+ * of them pass. The rules selected will be only those specified to be run on 'delete'
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
+ * @param array $options Extra options to pass to checker functions.
+ * @return bool
+ */
+ public function checkDelete(EntityInterface $entity, array $options = []): bool
+ {
+ return $this->_checkRules($entity, $options, $this->_deleteRules);
+ }
+
+ /**
+ * Used by top level functions checkDelete, checkCreate and checkUpdate, this function
+ * iterates an array containing the rules to be checked and checks them all.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check for validity.
+ * @param array $options Extra options to pass to checker functions.
+ * @param \Cake\Datasource\RuleInvoker[] $rules The list of rules that must be checked.
+ * @return bool
+ */
+ protected function _checkRules(EntityInterface $entity, array $options = [], array $rules = []): bool
+ {
+ $success = true;
+ $options += $this->_options;
+ foreach ($rules as $rule) {
+ $success = $rule($entity, $options) && $success;
+ }
+
+ return $success;
+ }
+
+ /**
+ * Utility method for decorating any callable so that if it returns false, the correct
+ * property in the entity is marked as invalid.
+ *
+ * @param callable $rule The rule to decorate
+ * @param string|array|null $name The alias for a rule or an array of options
+ * @param array $options The options containing the error message and field.
+ * @return \Cake\Datasource\RuleInvoker
+ */
+ protected function _addError(callable $rule, $name = null, array $options = []): RuleInvoker
+ {
+ if (is_array($name)) {
+ $options = $name;
+ $name = null;
+ }
+
+ if (!($rule instanceof RuleInvoker)) {
+ $rule = new RuleInvoker($rule, $name, $options);
+ } else {
+ $rule->setOptions($options)->setName($name);
+ }
+
+ return $rule;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Datasource/SchemaInterface.php b/app/vendor/cakephp/cakephp/src/Datasource/SchemaInterface.php
new file mode 100644
index 000000000..fdcb7a8ef
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Datasource/SchemaInterface.php
@@ -0,0 +1,166 @@
+=7.2.0",
+ "cakephp/core": "^4.0",
+ "psr/log": "^1.1",
+ "psr/simple-cache": "^1.0"
+ },
+ "suggest": {
+ "cakephp/utility": "If you decide to use EntityTrait.",
+ "cakephp/collection": "If you decide to use ResultSetInterface.",
+ "cakephp/cache": "If you decide to use Query caching."
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Datasource\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/BaseErrorHandler.php b/app/vendor/cakephp/cakephp/src/Error/BaseErrorHandler.php
new file mode 100644
index 000000000..6f55b2947
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/BaseErrorHandler.php
@@ -0,0 +1,402 @@
+ true,
+ 'trace' => false,
+ 'skipLog' => [],
+ 'errorLogger' => ErrorLogger::class,
+ ];
+
+ /**
+ * @var bool
+ */
+ protected $_handled = false;
+
+ /**
+ * Exception logger instance.
+ *
+ * @var \Cake\Error\ErrorLoggerInterface|null
+ */
+ protected $logger;
+
+ /**
+ * Display an error message in an environment specific way.
+ *
+ * Subclasses should implement this method to display the error as
+ * desired for the runtime they operate in.
+ *
+ * @param array $error An array of error data.
+ * @param bool $debug Whether or not the app is in debug mode.
+ * @return void
+ */
+ abstract protected function _displayError(array $error, bool $debug): void;
+
+ /**
+ * Display an exception in an environment specific way.
+ *
+ * Subclasses should implement this method to display an uncaught exception as
+ * desired for the runtime they operate in.
+ *
+ * @param \Throwable $exception The uncaught exception.
+ * @return void
+ */
+ abstract protected function _displayException(Throwable $exception): void;
+
+ /**
+ * Register the error and exception handlers.
+ *
+ * @return void
+ */
+ public function register(): void
+ {
+ $level = -1;
+ if (isset($this->_config['errorLevel'])) {
+ $level = $this->_config['errorLevel'];
+ }
+ error_reporting($level);
+ set_error_handler([$this, 'handleError'], $level);
+ set_exception_handler([$this, 'handleException']);
+ register_shutdown_function(function (): void {
+ if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && $this->_handled) {
+ return;
+ }
+ $megabytes = $this->_config['extraFatalErrorMemory'] ?? 4;
+ if ($megabytes > 0) {
+ $this->increaseMemoryLimit($megabytes * 1024);
+ }
+ $error = error_get_last();
+ if (!is_array($error)) {
+ return;
+ }
+ $fatals = [
+ E_USER_ERROR,
+ E_ERROR,
+ E_PARSE,
+ ];
+ if (!in_array($error['type'], $fatals, true)) {
+ return;
+ }
+ $this->handleFatalError(
+ $error['type'],
+ $error['message'],
+ $error['file'],
+ $error['line']
+ );
+ });
+ }
+
+ /**
+ * Set as the default error handler by CakePHP.
+ *
+ * Use config/error.php to customize or replace this error handler.
+ * This function will use Debugger to display errors when debug mode is on. And
+ * will log errors to Log, when debug mode is off.
+ *
+ * You can use the 'errorLevel' option to set what type of errors will be handled.
+ * Stack traces for errors can be enabled with the 'trace' option.
+ *
+ * @param int $code Code of error
+ * @param string $description Error description
+ * @param string|null $file File on which error occurred
+ * @param int|null $line Line that triggered the error
+ * @param array|null $context Context
+ * @return bool True if error was handled
+ */
+ public function handleError(
+ int $code,
+ string $description,
+ ?string $file = null,
+ ?int $line = null,
+ ?array $context = null
+ ): bool {
+ if (!(error_reporting() & $code)) {
+ return false;
+ }
+ $this->_handled = true;
+ [$error, $log] = static::mapErrorCode($code);
+ if ($log === LOG_ERR) {
+ /** @psalm-suppress PossiblyNullArgument */
+ return $this->handleFatalError($code, $description, $file, $line);
+ }
+ $data = [
+ 'level' => $log,
+ 'code' => $code,
+ 'error' => $error,
+ 'description' => $description,
+ 'file' => $file,
+ 'line' => $line,
+ ];
+
+ $debug = (bool)Configure::read('debug');
+ if ($debug) {
+ // By default trim 3 frames off for the public and protected methods
+ // used by ErrorHandler instances.
+ $start = 3;
+
+ // Can be used by error handlers that wrap other error handlers
+ // to coerce the generated stack trace to the correct point.
+ if (isset($context['_trace_frame_offset'])) {
+ $start += $context['_trace_frame_offset'];
+ unset($context['_trace_frame_offset']);
+ }
+ $data += [
+ 'context' => $context,
+ 'start' => $start,
+ 'path' => Debugger::trimPath((string)$file),
+ ];
+ }
+ $this->_displayError($data, $debug);
+ $this->_logError($log, $data);
+
+ return true;
+ }
+
+ /**
+ * Checks the passed exception type. If it is an instance of `Error`
+ * then, it wraps the passed object inside another Exception object
+ * for backwards compatibility purposes.
+ *
+ * @param \Throwable $exception The exception to handle
+ * @return void
+ * @deprecated 4.0.0 Unused method will be removed in 5.0
+ */
+ public function wrapAndHandleException(Throwable $exception): void
+ {
+ deprecationWarning('This method is no longer in use. Call handleException instead.');
+ $this->handleException($exception);
+ }
+
+ /**
+ * Handle uncaught exceptions.
+ *
+ * Uses a template method provided by subclasses to display errors in an
+ * environment appropriate way.
+ *
+ * @param \Throwable $exception Exception instance.
+ * @return void
+ * @throws \Exception When renderer class not found
+ * @see https://secure.php.net/manual/en/function.set-exception-handler.php
+ */
+ public function handleException(Throwable $exception): void
+ {
+ $this->_displayException($exception);
+ $this->logException($exception);
+ $code = $exception->getCode() ?: 1;
+ $this->_stop((int)$code);
+ }
+
+ /**
+ * Stop the process.
+ *
+ * Implemented in subclasses that need it.
+ *
+ * @param int $code Exit code.
+ * @return void
+ */
+ protected function _stop(int $code): void
+ {
+ // Do nothing.
+ }
+
+ /**
+ * Display/Log a fatal error.
+ *
+ * @param int $code Code of error
+ * @param string $description Error description
+ * @param string $file File on which error occurred
+ * @param int $line Line that triggered the error
+ * @return bool
+ */
+ public function handleFatalError(int $code, string $description, string $file, int $line): bool
+ {
+ $data = [
+ 'code' => $code,
+ 'description' => $description,
+ 'file' => $file,
+ 'line' => $line,
+ 'error' => 'Fatal Error',
+ ];
+ $this->_logError(LOG_ERR, $data);
+
+ $this->handleException(new FatalErrorException($description, 500, $file, $line));
+
+ return true;
+ }
+
+ /**
+ * Increases the PHP "memory_limit" ini setting by the specified amount
+ * in kilobytes
+ *
+ * @param int $additionalKb Number in kilobytes
+ * @return void
+ */
+ public function increaseMemoryLimit(int $additionalKb): void
+ {
+ $limit = ini_get('memory_limit');
+ if (!strlen($limit) || $limit === '-1') {
+ return;
+ }
+ $limit = trim($limit);
+ $units = strtoupper(substr($limit, -1));
+ $current = (int)substr($limit, 0, strlen($limit) - 1);
+ if ($units === 'M') {
+ $current *= 1024;
+ $units = 'K';
+ }
+ if ($units === 'G') {
+ $current = $current * 1024 * 1024;
+ $units = 'K';
+ }
+
+ if ($units === 'K') {
+ ini_set('memory_limit', ceil($current + $additionalKb) . 'K');
+ }
+ }
+
+ /**
+ * Log an error.
+ *
+ * @param int|string $level The level name of the log.
+ * @param array $data Array of error data.
+ * @return bool
+ */
+ protected function _logError($level, array $data): bool
+ {
+ $message = sprintf(
+ '%s (%s): %s in [%s, line %s]',
+ $data['error'],
+ $data['code'],
+ $data['description'],
+ $data['file'],
+ $data['line']
+ );
+ $context = [];
+ if (!empty($this->_config['trace'])) {
+ $context['trace'] = Debugger::trace([
+ 'start' => 1,
+ 'format' => 'log',
+ ]);
+ $context['request'] = Router::getRequest();
+ }
+
+ return $this->getLogger()->logMessage($level, $message, $context);
+ }
+
+ /**
+ * Log an error for the exception if applicable.
+ *
+ * @param \Throwable $exception The exception to log a message for.
+ * @param \Psr\Http\Message\ServerRequestInterface $request The current request.
+ * @return bool
+ */
+ public function logException(Throwable $exception, ?ServerRequestInterface $request = null): bool
+ {
+ if (empty($this->_config['log'])) {
+ return false;
+ }
+
+ return $this->getLogger()->log($exception, $request ?? Router::getRequest());
+ }
+
+ /**
+ * Get exception logger.
+ *
+ * @return \Cake\Error\ErrorLoggerInterface
+ */
+ public function getLogger()
+ {
+ if ($this->logger === null) {
+ /** @var \Cake\Error\ErrorLoggerInterface $logger */
+ $logger = new $this->_config['errorLogger']($this->_config);
+
+ if (!$logger instanceof ErrorLoggerInterface) {
+ // Set the logger so that the next error can be logged.
+ $this->logger = new ErrorLogger($this->_config);
+
+ $interface = ErrorLoggerInterface::class;
+ $type = getTypeName($logger);
+ throw new RuntimeException("Cannot create logger. `{$type}` does not implement `{$interface}`.");
+ }
+ $this->logger = $logger;
+ }
+
+ return $this->logger;
+ }
+
+ /**
+ * Map an error code into an Error word, and log location.
+ *
+ * @param int $code Error code to map
+ * @return array Array of error word, and log location.
+ */
+ public static function mapErrorCode(int $code): array
+ {
+ $levelMap = [
+ E_PARSE => 'error',
+ E_ERROR => 'error',
+ E_CORE_ERROR => 'error',
+ E_COMPILE_ERROR => 'error',
+ E_USER_ERROR => 'error',
+ E_WARNING => 'warning',
+ E_USER_WARNING => 'warning',
+ E_COMPILE_WARNING => 'warning',
+ E_RECOVERABLE_ERROR => 'warning',
+ E_NOTICE => 'notice',
+ E_USER_NOTICE => 'notice',
+ E_STRICT => 'strict',
+ E_DEPRECATED => 'deprecated',
+ E_USER_DEPRECATED => 'deprecated',
+ ];
+ $logMap = [
+ 'error' => LOG_ERR,
+ 'warning' => LOG_WARNING,
+ 'notice' => LOG_NOTICE,
+ 'strict' => LOG_NOTICE,
+ 'deprecated' => LOG_NOTICE,
+ ];
+
+ $error = $levelMap[$code];
+ $log = $logMap[$error];
+
+ return [ucfirst($error), $log];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/ConsoleErrorHandler.php b/app/vendor/cakephp/cakephp/src/Error/ConsoleErrorHandler.php
new file mode 100644
index 000000000..6eff9ec5c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/ConsoleErrorHandler.php
@@ -0,0 +1,132 @@
+ new ConsoleOutput('php://stderr'),
+ 'log' => false,
+ ];
+
+ $this->setConfig($config);
+ $this->_stderr = $this->_config['stderr'];
+ }
+
+ /**
+ * Handle errors in the console environment. Writes errors to stderr,
+ * and logs messages if Configure::read('debug') is false.
+ *
+ * @param \Throwable $exception Exception instance.
+ * @return void
+ * @throws \Exception When renderer class not found
+ * @see https://secure.php.net/manual/en/function.set-exception-handler.php
+ */
+ public function handleException(Throwable $exception): void
+ {
+ $this->_displayException($exception);
+ $this->logException($exception);
+
+ $exitCode = Command::CODE_ERROR;
+ if ($exception instanceof ConsoleException) {
+ $exitCode = $exception->getCode();
+ }
+ $this->_stop($exitCode);
+ }
+
+ /**
+ * Prints an exception to stderr.
+ *
+ * @param \Throwable $exception The exception to handle
+ * @return void
+ */
+ protected function _displayException(Throwable $exception): void
+ {
+ $errorName = 'Exception:';
+ if ($exception instanceof FatalErrorException) {
+ $errorName = 'Fatal Error:';
+ }
+
+ $message = sprintf(
+ "%s %s\nIn [%s, line %s]\n",
+ $errorName,
+ $exception->getMessage(),
+ $exception->getFile(),
+ $exception->getLine()
+ );
+ $this->_stderr->write($message);
+ }
+
+ /**
+ * Prints an error to stderr.
+ *
+ * Template method of BaseErrorHandler.
+ *
+ * @param array $error An array of error data.
+ * @param bool $debug Whether or not the app is in debug mode.
+ * @return void
+ */
+ protected function _displayError(array $error, bool $debug): void
+ {
+ $message = sprintf(
+ "%s\nIn [%s, line %s]",
+ $error['description'],
+ $error['file'],
+ $error['line']
+ );
+ $message = sprintf(
+ "%s Error: %s\n",
+ $error['error'],
+ $message
+ );
+ $this->_stderr->write($message);
+ }
+
+ /**
+ * Stop the execution and set the exit code for the process.
+ *
+ * @param int $code The exit code.
+ * @return void
+ */
+ protected function _stop(int $code): void
+ {
+ exit($code);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/ArrayItemNode.php b/app/vendor/cakephp/cakephp/src/Error/Debug/ArrayItemNode.php
new file mode 100644
index 000000000..4051dd475
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/ArrayItemNode.php
@@ -0,0 +1,73 @@
+key = $key;
+ $this->value = $value;
+ }
+
+ /**
+ * Get the value
+ *
+ * @return \Cake\Error\Debug\NodeInterface
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Get the key
+ *
+ * @return \Cake\Error\Debug\NodeInterface
+ */
+ public function getKey()
+ {
+ return $this->key;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getChildren(): array
+ {
+ return [$this->value];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/ArrayNode.php b/app/vendor/cakephp/cakephp/src/Error/Debug/ArrayNode.php
new file mode 100644
index 000000000..115a40f16
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/ArrayNode.php
@@ -0,0 +1,72 @@
+items = [];
+ foreach ($items as $item) {
+ $this->add($item);
+ }
+ }
+
+ /**
+ * Add an item
+ *
+ * @param \Cake\Error\Debug\ArrayItemNode $node The item to add.
+ * @return void
+ */
+ public function add(ArrayItemNode $node): void
+ {
+ $this->items[] = $node;
+ }
+
+ /**
+ * Get the contained items
+ *
+ * @return \Cake\Error\Debug\ArrayItemNode[]
+ */
+ public function getValue(): array
+ {
+ return $this->items;
+ }
+
+ /**
+ * Get Item nodes
+ *
+ * @return \Cake\Error\Debug\ArrayItemNode[]
+ */
+ public function getChildren(): array
+ {
+ return $this->items;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/ClassNode.php b/app/vendor/cakephp/cakephp/src/Error/Debug/ClassNode.php
new file mode 100644
index 000000000..d97ea328f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/ClassNode.php
@@ -0,0 +1,91 @@
+class = $class;
+ $this->id = $id;
+ }
+
+ /**
+ * Add a property
+ *
+ * @param \Cake\Error\Debug\PropertyNode $node The property to add.
+ * @return void
+ */
+ public function addProperty(PropertyNode $node): void
+ {
+ $this->properties[] = $node;
+ }
+
+ /**
+ * Get the class name
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->class;
+ }
+
+ /**
+ * Get the reference id
+ *
+ * @return int
+ */
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ /**
+ * Get property nodes
+ *
+ * @return \Cake\Error\Debug\PropertyNode[]
+ */
+ public function getChildren(): array
+ {
+ return $this->properties;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/ConsoleFormatter.php b/app/vendor/cakephp/cakephp/src/Error/Debug/ConsoleFormatter.php
new file mode 100644
index 000000000..5f1c6bd89
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/ConsoleFormatter.php
@@ -0,0 +1,243 @@
+ '1;33',
+ // green
+ 'string' => '0;32',
+ // bold blue
+ 'number' => '1;34',
+ // cyan
+ 'class' => '0;36',
+ // grey
+ 'punct' => '0;90',
+ // default foreground
+ 'property' => '0;39',
+ // magenta
+ 'visibility' => '0;35',
+ // red
+ 'special' => '0;31',
+ ];
+
+ /**
+ * Check if the current environment supports ANSI output.
+ *
+ * @return bool
+ */
+ public static function environmentMatches(): bool
+ {
+ if (PHP_SAPI !== 'cli') {
+ return false;
+ }
+ // NO_COLOR in environment means no color.
+ if (env('NO_COLOR')) {
+ return false;
+ }
+ // Windows environment checks
+ if (
+ DIRECTORY_SEPARATOR === '\\' &&
+ strpos(strtolower(php_uname('v')), 'windows 10') === false &&
+ strpos(strtolower((string)env('SHELL')), 'bash.exe') === false &&
+ !(bool)env('ANSICON') &&
+ env('ConEmuANSI') !== 'ON'
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function formatWrapper(string $contents, array $location): string
+ {
+ $lineInfo = '';
+ if (isset($location['file'], $location['file'])) {
+ $lineInfo = sprintf('%s (line %s)', $location['file'], $location['line']);
+ }
+ $parts = [
+ $this->style('const', $lineInfo),
+ $this->style('special', '########## DEBUG ##########'),
+ $contents,
+ $this->style('special', '###########################'),
+ '',
+ ];
+
+ return implode("\n", $parts);
+ }
+
+ /**
+ * Convert a tree of NodeInterface objects into a plain text string.
+ *
+ * @param \Cake\Error\Debug\NodeInterface $node The node tree to dump.
+ * @return string
+ */
+ public function dump(NodeInterface $node): string
+ {
+ $indent = 0;
+
+ return $this->export($node, $indent);
+ }
+
+ /**
+ * Convert a tree of NodeInterface objects into a plain text string.
+ *
+ * @param \Cake\Error\Debug\NodeInterface $var The node tree to dump.
+ * @param int $indent The current indentation level.
+ * @return string
+ */
+ protected function export(NodeInterface $var, int $indent): string
+ {
+ if ($var instanceof ScalarNode) {
+ switch ($var->getType()) {
+ case 'bool':
+ return $this->style('const', $var->getValue() ? 'true' : 'false');
+ case 'null':
+ return $this->style('const', 'null');
+ case 'string':
+ return $this->style('string', "'" . (string)$var->getValue() . "'");
+ case 'int':
+ case 'float':
+ return $this->style('visibility', "({$var->getType()})") .
+ ' ' . $this->style('number', "{$var->getValue()}");
+ default:
+ return "({$var->getType()}) {$var->getValue()}";
+ }
+ }
+ if ($var instanceof ArrayNode) {
+ return $this->exportArray($var, $indent + 1);
+ }
+ if ($var instanceof ClassNode || $var instanceof ReferenceNode) {
+ return $this->exportObject($var, $indent + 1);
+ }
+ if ($var instanceof SpecialNode) {
+ return $this->style('special', $var->getValue());
+ }
+ throw new RuntimeException('Unknown node received ' . get_class($var));
+ }
+
+ /**
+ * Export an array type object
+ *
+ * @param \Cake\Error\Debug\ArrayNode $var The array to export.
+ * @param int $indent The current indentation level.
+ * @return string Exported array.
+ */
+ protected function exportArray(ArrayNode $var, int $indent): string
+ {
+ $out = $this->style('punct', '[');
+ $break = "\n" . str_repeat(' ', $indent);
+ $end = "\n" . str_repeat(' ', $indent - 1);
+ $vars = [];
+
+ $arrow = $this->style('punct', ' => ');
+ foreach ($var->getChildren() as $item) {
+ $val = $item->getValue();
+ $vars[] = $break . $this->export($item->getKey(), $indent) . $arrow . $this->export($val, $indent);
+ }
+
+ $close = $this->style('punct', ']');
+ if (count($vars)) {
+ return $out . implode($this->style('punct', ','), $vars) . $end . $close;
+ }
+
+ return $out . $close;
+ }
+
+ /**
+ * Handles object to string conversion.
+ *
+ * @param \Cake\Error\Debug\ClassNode|\Cake\Error\Debug\ReferenceNode $var Object to convert.
+ * @param int $indent Current indentation level.
+ * @return string
+ * @see \Cake\Error\Debugger::exportVar()
+ */
+ protected function exportObject($var, int $indent): string
+ {
+ $props = [];
+
+ if ($var instanceof ReferenceNode) {
+ return $this->style('punct', 'object(') .
+ $this->style('class', $var->getValue()) .
+ $this->style('punct', ') id:') .
+ $this->style('number', (string)$var->getId()) .
+ $this->style('punct', ' {}');
+ }
+
+ $out = $this->style('punct', 'object(') .
+ $this->style('class', $var->getValue()) .
+ $this->style('punct', ') id:') .
+ $this->style('number', (string)$var->getId()) .
+ $this->style('punct', ' {');
+
+ $break = "\n" . str_repeat(' ', $indent);
+ $end = "\n" . str_repeat(' ', $indent - 1) . $this->style('punct', '}');
+
+ $arrow = $this->style('punct', ' => ');
+ foreach ($var->getChildren() as $property) {
+ $visibility = $property->getVisibility();
+ $name = $property->getName();
+ if ($visibility && $visibility !== 'public') {
+ $props[] = $this->style('visibility', $visibility) .
+ ' ' .
+ $this->style('property', $name) .
+ $arrow .
+ $this->export($property->getValue(), $indent);
+ } else {
+ $props[] = $this->style('property', $name) .
+ $arrow .
+ $this->export($property->getValue(), $indent);
+ }
+ }
+ if (count($props)) {
+ return $out . $break . implode($break, $props) . $end;
+ }
+
+ return $out . $this->style('punct', '}');
+ }
+
+ /**
+ * Style text with ANSI escape codes.
+ *
+ * @param string $style The style name to use.
+ * @param string $text The text to style.
+ * @return string The styled output.
+ */
+ protected function style(string $style, string $text): string
+ {
+ $code = $this->styles[$style];
+
+ return "\033[{$code}m{$text}\033[0m";
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/DebugContext.php b/app/vendor/cakephp/cakephp/src/Error/Debug/DebugContext.php
new file mode 100644
index 000000000..7eb21bf57
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/DebugContext.php
@@ -0,0 +1,110 @@
+maxDepth = $maxDepth;
+ $this->refs = new SplObjectStorage();
+ }
+
+ /**
+ * Return a clone with increased depth.
+ *
+ * @return static
+ */
+ public function withAddedDepth()
+ {
+ $new = clone $this;
+ $new->depth += 1;
+
+ return $new;
+ }
+
+ /**
+ * Get the remaining depth levels
+ *
+ * @return int
+ */
+ public function remainingDepth(): int
+ {
+ return $this->maxDepth - $this->depth;
+ }
+
+ /**
+ * Get the reference ID for an object.
+ *
+ * If this object does not exist in the reference storage,
+ * it will be added and the id will be returned.
+ *
+ * @param object $object The object to get a reference for.
+ * @return int
+ */
+ public function getReferenceId(object $object): int
+ {
+ if ($this->refs->contains($object)) {
+ return $this->refs[$object];
+ }
+ $refId = $this->refs->count();
+ $this->refs->attach($object, $refId);
+
+ return $refId;
+ }
+
+ /**
+ * Check whether an object has been seen before.
+ *
+ * @param object $object The object to get a reference for.
+ * @return bool
+ */
+ public function hasReference(object $object): bool
+ {
+ return $this->refs->contains($object);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/FormatterInterface.php b/app/vendor/cakephp/cakephp/src/Error/Debug/FormatterInterface.php
new file mode 100644
index 000000000..bacf883d3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/FormatterInterface.php
@@ -0,0 +1,42 @@
+id = uniqid('', true);
+ }
+
+ /**
+ * Check if the current environment is not a CLI context
+ *
+ * @return bool
+ */
+ public static function environmentMatches(): bool
+ {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function formatWrapper(string $contents, array $location): string
+ {
+ $lineInfo = '';
+ if (isset($location['file'], $location['file'])) {
+ $lineInfo = sprintf(
+ '%s (line %s)',
+ $location['file'],
+ $location['line']
+ );
+ }
+ $parts = [
+ '',
+ $lineInfo,
+ $contents,
+ '',
+ ];
+
+ return implode("\n", $parts);
+ }
+
+ /**
+ * Generate the CSS and Javascript for dumps
+ *
+ * Only output once per process as we don't need it more than once.
+ *
+ * @return string
+ */
+ protected function dumpHeader(): string
+ {
+ ob_start();
+ include __DIR__ . DIRECTORY_SEPARATOR . 'dumpHeader.html';
+
+ return ob_get_clean();
+ }
+
+ /**
+ * Convert a tree of NodeInterface objects into HTML
+ *
+ * @param \Cake\Error\Debug\NodeInterface $node The node tree to dump.
+ * @return string
+ */
+ public function dump(NodeInterface $node): string
+ {
+ $html = $this->export($node, 0);
+ $head = '';
+ if (!static::$outputHeader) {
+ static::$outputHeader = true;
+ $head = $this->dumpHeader();
+ }
+
+ return $head . '' . $html . '';
+ }
+
+ /**
+ * Convert a tree of NodeInterface objects into HTML
+ *
+ * @param \Cake\Error\Debug\NodeInterface $var The node tree to dump.
+ * @param int $indent The current indentation level.
+ * @return string
+ */
+ protected function export(NodeInterface $var, int $indent): string
+ {
+ if ($var instanceof ScalarNode) {
+ switch ($var->getType()) {
+ case 'bool':
+ return $this->style('const', $var->getValue() ? 'true' : 'false');
+ case 'null':
+ return $this->style('const', 'null');
+ case 'string':
+ return $this->style('string', "'" . (string)$var->getValue() . "'");
+ case 'int':
+ case 'float':
+ return $this->style('visibility', "({$var->getType()})") .
+ ' ' . $this->style('number', "{$var->getValue()}");
+ default:
+ return "({$var->getType()}) {$var->getValue()}";
+ }
+ }
+ if ($var instanceof ArrayNode) {
+ return $this->exportArray($var, $indent + 1);
+ }
+ if ($var instanceof ClassNode || $var instanceof ReferenceNode) {
+ return $this->exportObject($var, $indent + 1);
+ }
+ if ($var instanceof SpecialNode) {
+ return $this->style('special', $var->getValue());
+ }
+ throw new RuntimeException('Unknown node received ' . get_class($var));
+ }
+
+ /**
+ * Export an array type object
+ *
+ * @param \Cake\Error\Debug\ArrayNode $var The array to export.
+ * @param int $indent The current indentation level.
+ * @return string Exported array.
+ */
+ protected function exportArray(ArrayNode $var, int $indent): string
+ {
+ $open = '' .
+ $this->style('punct', '[') .
+ '';
+ $vars = [];
+ $break = "\n" . str_repeat(' ', $indent);
+ $endBreak = "\n" . str_repeat(' ', $indent - 1);
+
+ $arrow = $this->style('punct', ' => ');
+ foreach ($var->getChildren() as $item) {
+ $val = $item->getValue();
+ $vars[] = $break . '' .
+ $this->export($item->getKey(), $indent) . $arrow . $this->export($val, $indent) .
+ $this->style('punct', ',') .
+ '';
+ }
+
+ $close = '' .
+ $endBreak .
+ $this->style('punct', ']') .
+ '';
+
+ return $open . implode('', $vars) . $close;
+ }
+
+ /**
+ * Handles object to string conversion.
+ *
+ * @param \Cake\Error\Debug\ClassNode|\Cake\Error\Debug\ReferenceNode $var Object to convert.
+ * @param int $indent The current indentation level.
+ * @return string
+ * @see \Cake\Error\Debugger::exportVar()
+ */
+ protected function exportObject($var, int $indent): string
+ {
+ $objectId = "cake-db-object-{$this->id}-{$var->getId()}";
+ $out = sprintf(
+ '',
+ $objectId
+ );
+ $break = "\n" . str_repeat(' ', $indent);
+ $endBreak = "\n" . str_repeat(' ', $indent - 1);
+
+ if ($var instanceof ReferenceNode) {
+ $link = sprintf(
+ 'id: %s',
+ $objectId,
+ $var->getId()
+ );
+
+ return '' .
+ $this->style('punct', 'object(') .
+ $this->style('class', $var->getValue()) .
+ $this->style('punct', ') ') .
+ $link .
+ $this->style('punct', ' {}') .
+ '';
+ }
+
+ $out .= $this->style('punct', 'object(') .
+ $this->style('class', $var->getValue()) .
+ $this->style('punct', ') id:') .
+ $this->style('number', (string)$var->getId()) .
+ $this->style('punct', ' {') .
+ '';
+
+ $props = [];
+ foreach ($var->getChildren() as $property) {
+ $arrow = $this->style('punct', ' => ');
+ $visibility = $property->getVisibility();
+ $name = $property->getName();
+ if ($visibility && $visibility !== 'public') {
+ $props[] = $break .
+ '' .
+ $this->style('visibility', $visibility) .
+ ' ' .
+ $this->style('property', $name) .
+ $arrow .
+ $this->export($property->getValue(), $indent) .
+ '';
+ } else {
+ $props[] = $break .
+ '' .
+ $this->style('property', $name) .
+ $arrow .
+ $this->export($property->getValue(), $indent) .
+ '';
+ }
+ }
+
+ $end = '' .
+ $endBreak .
+ $this->style('punct', '}') .
+ '';
+
+ if (count($props)) {
+ return $out . implode('', $props) . $end;
+ }
+
+ return $out . $end;
+ }
+
+ /**
+ * Style text with HTML class names
+ *
+ * @param string $style The style name to use.
+ * @param string $text The text to style.
+ * @return string The styled output.
+ */
+ protected function style(string $style, string $text): string
+ {
+ return sprintf(
+ '%s',
+ $style,
+ h($text)
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/NodeInterface.php b/app/vendor/cakephp/cakephp/src/Error/Debug/NodeInterface.php
new file mode 100644
index 000000000..82e25c484
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/NodeInterface.php
@@ -0,0 +1,39 @@
+name = $name;
+ $this->visibility = $visibility;
+ $this->value = $value;
+ }
+
+ /**
+ * Get the value
+ *
+ * @return \Cake\Error\Debug\NodeInterface
+ */
+ public function getValue(): NodeInterface
+ {
+ return $this->value;
+ }
+
+ /**
+ * Get the property visibility
+ *
+ * @return string
+ */
+ public function getVisibility(): ?string
+ {
+ return $this->visibility;
+ }
+
+ /**
+ * Get the property name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getChildren(): array
+ {
+ return [$this->value];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/ReferenceNode.php b/app/vendor/cakephp/cakephp/src/Error/Debug/ReferenceNode.php
new file mode 100644
index 000000000..008428f42
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/ReferenceNode.php
@@ -0,0 +1,77 @@
+class = $class;
+ $this->id = $id;
+ }
+
+ /**
+ * Get the class name/value
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->class;
+ }
+
+ /**
+ * Get the reference id for this node.
+ *
+ * @return int
+ */
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getChildren(): array
+ {
+ return [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/ScalarNode.php b/app/vendor/cakephp/cakephp/src/Error/Debug/ScalarNode.php
new file mode 100644
index 000000000..cb4098d86
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/ScalarNode.php
@@ -0,0 +1,73 @@
+type = $type;
+ $this->value = $value;
+ }
+
+ /**
+ * Get the type of value
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ /**
+ * Get the value
+ *
+ * @return string|int|float|bool|null
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getChildren(): array
+ {
+ return [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/SpecialNode.php b/app/vendor/cakephp/cakephp/src/Error/Debug/SpecialNode.php
new file mode 100644
index 000000000..34b38f0b8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/SpecialNode.php
@@ -0,0 +1,56 @@
+value = $value;
+ }
+
+ /**
+ * Get the message/value
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getChildren(): array
+ {
+ return [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/TextFormatter.php b/app/vendor/cakephp/cakephp/src/Error/Debug/TextFormatter.php
new file mode 100644
index 000000000..ef60b74d1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/TextFormatter.php
@@ -0,0 +1,158 @@
+export($node, $indent);
+ }
+
+ /**
+ * Convert a tree of NodeInterface objects into a plain text string.
+ *
+ * @param \Cake\Error\Debug\NodeInterface $var The node tree to dump.
+ * @param int $indent The current indentation level.
+ * @return string
+ */
+ protected function export(NodeInterface $var, int $indent): string
+ {
+ if ($var instanceof ScalarNode) {
+ switch ($var->getType()) {
+ case 'bool':
+ return $var->getValue() ? 'true' : 'false';
+ case 'null':
+ return 'null';
+ case 'string':
+ return "'" . (string)$var->getValue() . "'";
+ default:
+ return "({$var->getType()}) {$var->getValue()}";
+ }
+ }
+ if ($var instanceof ArrayNode) {
+ return $this->exportArray($var, $indent + 1);
+ }
+ if ($var instanceof ClassNode || $var instanceof ReferenceNode) {
+ return $this->exportObject($var, $indent + 1);
+ }
+ if ($var instanceof SpecialNode) {
+ return $var->getValue();
+ }
+ throw new RuntimeException('Unknown node received ' . get_class($var));
+ }
+
+ /**
+ * Export an array type object
+ *
+ * @param \Cake\Error\Debug\ArrayNode $var The array to export.
+ * @param int $indent The current indentation level.
+ * @return string Exported array.
+ */
+ protected function exportArray(ArrayNode $var, int $indent): string
+ {
+ $out = '[';
+ $break = "\n" . str_repeat(' ', $indent);
+ $end = "\n" . str_repeat(' ', $indent - 1);
+ $vars = [];
+
+ foreach ($var->getChildren() as $item) {
+ $val = $item->getValue();
+ $vars[] = $break . $this->export($item->getKey(), $indent) . ' => ' . $this->export($val, $indent);
+ }
+ if (count($vars)) {
+ return $out . implode(',', $vars) . $end . ']';
+ }
+
+ return $out . ']';
+ }
+
+ /**
+ * Handles object to string conversion.
+ *
+ * @param \Cake\Error\Debug\ClassNode|\Cake\Error\Debug\ReferenceNode $var Object to convert.
+ * @param int $indent Current indentation level.
+ * @return string
+ * @see \Cake\Error\Debugger::exportVar()
+ */
+ protected function exportObject($var, int $indent): string
+ {
+ $out = '';
+ $props = [];
+
+ if ($var instanceof ReferenceNode) {
+ return "object({$var->getValue()}) id:{$var->getId()} {}";
+ }
+
+ $out .= "object({$var->getValue()}) id:{$var->getId()} {";
+ $break = "\n" . str_repeat(' ', $indent);
+ $end = "\n" . str_repeat(' ', $indent - 1) . '}';
+
+ foreach ($var->getChildren() as $property) {
+ $visibility = $property->getVisibility();
+ $name = $property->getName();
+ if ($visibility && $visibility !== 'public') {
+ $props[] = "[{$visibility}] {$name} => " . $this->export($property->getValue(), $indent);
+ } else {
+ $props[] = "{$name} => " . $this->export($property->getValue(), $indent);
+ }
+ }
+ if (count($props)) {
+ return $out . $break . implode($break, $props) . $end;
+ }
+
+ return $out . '}';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debug/dumpHeader.html b/app/vendor/cakephp/cakephp/src/Error/Debug/dumpHeader.html
new file mode 100644
index 000000000..501b2d0ad
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debug/dumpHeader.html
@@ -0,0 +1,275 @@
+
+
diff --git a/app/vendor/cakephp/cakephp/src/Error/Debugger.php b/app/vendor/cakephp/cakephp/src/Error/Debugger.php
new file mode 100644
index 000000000..7f8859ec7
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Debugger.php
@@ -0,0 +1,1070 @@
+ [],
+ 'exportFormatter' => null,
+ 'editor' => 'phpstorm',
+ ];
+
+ /**
+ * The current output format.
+ *
+ * @var string
+ */
+ protected $_outputFormat = 'js';
+
+ /**
+ * Templates used when generating trace or error strings. Can be global or indexed by the format
+ * value used in $_outputFormat.
+ *
+ * @var array
+ */
+ protected $_templates = [
+ 'log' => [
+ 'trace' => '{:reference} - {:path}, line {:line}',
+ 'error' => '{:error} ({:code}): {:description} in [{:file}, line {:line}]',
+ ],
+ 'js' => [
+ 'error' => '',
+ 'info' => '',
+ 'trace' => '{:trace}',
+ 'code' => '',
+ 'context' => '',
+ 'links' => [],
+ 'escapeContext' => true,
+ ],
+ 'html' => [
+ 'trace' => 'Trace {:trace}
',
+ 'context' => 'Context {:context}
',
+ 'escapeContext' => true,
+ ],
+ 'txt' => [
+ 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}",
+ 'code' => '',
+ 'info' => '',
+ ],
+ 'base' => [
+ 'traceLine' => '{:reference} - {:path}, line {:line}',
+ 'trace' => "Trace:\n{:trace}\n",
+ 'context' => "Context:\n{:context}\n",
+ ],
+ ];
+
+ /**
+ * A map of editors to their link templates.
+ *
+ * @var array
+ */
+ protected $editors = [
+ 'atom' => 'atom://core/open/file?filename={file}&line={line}',
+ 'emacs' => 'emacs://open?url=file://{file}&line={line}',
+ 'macvim' => 'mvim://open/?url=file://{file}&line={line}',
+ 'phpstorm' => 'phpstorm://open?file={file}&line={line}',
+ 'sublime' => 'subl://open?url=file://{file}&line={line}',
+ 'textmate' => 'txmt://open?url=file://{file}&line={line}',
+ 'vscode' => 'vscode://file/{file}:{line}',
+ ];
+
+ /**
+ * Holds current output data when outputFormat is false.
+ *
+ * @var array
+ */
+ protected $_data = [];
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ $docRef = ini_get('docref_root');
+ if (empty($docRef) && function_exists('ini_set')) {
+ ini_set('docref_root', 'https://secure.php.net/');
+ }
+ if (!defined('E_RECOVERABLE_ERROR')) {
+ define('E_RECOVERABLE_ERROR', 4096);
+ }
+
+ $config = array_intersect_key((array)Configure::read('Debugger'), $this->_defaultConfig);
+ $this->setConfig($config);
+
+ $e = '';
+ $e .= '{:error} ({:code}): {:description} ';
+ $e .= '[{:path}, line {:line}]';
+
+ $e .= '';
+ $e .= '';
+ $this->_templates['js']['error'] = $e;
+
+ $t = '';
+ $this->_templates['js']['info'] = $t;
+
+ $links = [];
+ $link = 'Code';
+ $links['code'] = $link;
+
+ $link = 'Context';
+ $links['context'] = $link;
+
+ $this->_templates['js']['links'] = $links;
+
+ $this->_templates['js']['context'] = '_templates['js']['context'] .= 'style="display: none;">{:context}';
+
+ $this->_templates['js']['code'] = '_templates['js']['code'] .= 'style="display: none;">{:code}';
+
+ $e = '{:error} ({:code}) : {:description} ';
+ $e .= '[{:path}, line {:line}]';
+ $this->_templates['html']['error'] = $e;
+
+ $this->_templates['html']['context'] = 'Context ';
+ $this->_templates['html']['context'] .= '{:context}
';
+ }
+
+ /**
+ * Returns a reference to the Debugger singleton object instance.
+ *
+ * @param string|null $class Class name.
+ * @return static
+ */
+ public static function getInstance(?string $class = null)
+ {
+ static $instance = [];
+ if (!empty($class)) {
+ if (!$instance || strtolower($class) !== strtolower(get_class($instance[0]))) {
+ $instance[0] = new $class();
+ }
+ }
+ if (!$instance) {
+ $instance[0] = new Debugger();
+ }
+
+ return $instance[0];
+ }
+
+ /**
+ * Read or write configuration options for the Debugger instance.
+ *
+ * @param string|array|null $key The key to get/set, or a complete array of configs.
+ * @param mixed|null $value The value to set.
+ * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
+ * @return mixed Config value being read, or the object itself on write operations.
+ * @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid.
+ */
+ public static function configInstance($key = null, $value = null, bool $merge = true)
+ {
+ if ($key === null) {
+ return static::getInstance()->getConfig($key);
+ }
+
+ if (is_array($key) || func_num_args() >= 2) {
+ return static::getInstance()->setConfig($key, $value, $merge);
+ }
+
+ return static::getInstance()->getConfig($key);
+ }
+
+ /**
+ * Reads the current output masking.
+ *
+ * @return array
+ */
+ public static function outputMask(): array
+ {
+ return static::configInstance('outputMask');
+ }
+
+ /**
+ * Sets configurable masking of debugger output by property name and array key names.
+ *
+ * ### Example
+ *
+ * Debugger::setOutputMask(['password' => '[*************]');
+ *
+ * @param array $value An array where keys are replaced by their values in output.
+ * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
+ * @return void
+ */
+ public static function setOutputMask(array $value, bool $merge = true): void
+ {
+ static::configInstance('outputMask', $value, $merge);
+ }
+
+ /**
+ * Add an editor link format
+ *
+ * Template strings can use the `{file}` and `{line}` placeholders.
+ * Closures templates must return a string, and accept two parameters:
+ * The file and line.
+ *
+ * @param string $name The name of the editor.
+ * @param string|\Closure $template The string template or closure
+ * @return void
+ */
+ public static function addEditor(string $name, $template): void
+ {
+ $instance = static::getInstance();
+ if (!is_string($template) && !($template instanceof Closure)) {
+ $type = getTypeName($template);
+ throw new RuntimeException("Invalid editor type of `{$type}`. Expected string or Closure.");
+ }
+ $instance->editors[$name] = $template;
+ }
+
+ /**
+ * Choose the editor link style you want to use.
+ *
+ * @param string $name The editor name.
+ * @return void
+ */
+ public static function setEditor(string $name): void
+ {
+ $instance = static::getInstance();
+ if (!isset($instance->editors[$name])) {
+ $known = implode(', ', array_keys($instance->editors));
+ throw new RuntimeException("Unknown editor `{$name}`. Known editors are {$known}");
+ }
+ $instance->setConfig('editor', $name);
+ }
+
+ /**
+ * Get a formatted URL for the active editor.
+ *
+ * @param string $file The file to create a link for.
+ * @param int $line The line number to create a link for.
+ * @return string The formatted URL.
+ */
+ public static function editorUrl(string $file, int $line): string
+ {
+ $instance = static::getInstance();
+ $editor = $instance->getConfig('editor');
+ if (!isset($instance->editors[$editor])) {
+ throw new RuntimeException("Cannot format editor URL `{$editor}` is not a known editor.");
+ }
+
+ $template = $instance->editors[$editor];
+ if (is_string($template)) {
+ return str_replace(['{file}', '{line}'], [$file, (string)$line], $template);
+ }
+
+ return $template($file, $line);
+ }
+
+ /**
+ * Recursively formats and outputs the contents of the supplied variable.
+ *
+ * @param mixed $var The variable to dump.
+ * @param int $maxDepth The depth to output to. Defaults to 3.
+ * @return void
+ * @see \Cake\Error\Debugger::exportVar()
+ * @link https://book.cakephp.org/4/en/development/debugging.html#outputting-values
+ */
+ public static function dump($var, int $maxDepth = 3): void
+ {
+ pr(static::exportVar($var, $maxDepth));
+ }
+
+ /**
+ * Creates an entry in the log file. The log entry will contain a stack trace from where it was called.
+ * as well as export the variable using exportVar. By default the log is written to the debug log.
+ *
+ * @param mixed $var Variable or content to log.
+ * @param int|string $level Type of log to use. Defaults to 'debug'.
+ * @param int $maxDepth The depth to output to. Defaults to 3.
+ * @return void
+ */
+ public static function log($var, $level = 'debug', int $maxDepth = 3): void
+ {
+ /** @var string $source */
+ $source = static::trace(['start' => 1]);
+ $source .= "\n";
+
+ Log::write($level, "\n" . $source . static::exportVar($var, $maxDepth));
+ }
+
+ /**
+ * Outputs a stack trace based on the supplied options.
+ *
+ * ### Options
+ *
+ * - `depth` - The number of stack frames to return. Defaults to 999
+ * - `format` - The format you want the return. Defaults to the currently selected format. If
+ * format is 'array' or 'points' the return will be an array.
+ * - `args` - Should arguments for functions be shown? If true, the arguments for each method call
+ * will be displayed.
+ * - `start` - The stack frame to start generating a trace from. Defaults to 0
+ *
+ * @param array $options Format for outputting stack trace.
+ * @return string|array Formatted stack trace.
+ * @link https://book.cakephp.org/4/en/development/debugging.html#generating-stack-traces
+ */
+ public static function trace(array $options = [])
+ {
+ return Debugger::formatTrace(debug_backtrace(), $options);
+ }
+
+ /**
+ * Formats a stack trace based on the supplied options.
+ *
+ * ### Options
+ *
+ * - `depth` - The number of stack frames to return. Defaults to 999
+ * - `format` - The format you want the return. Defaults to the currently selected format. If
+ * format is 'array' or 'points' the return will be an array.
+ * - `args` - Should arguments for functions be shown? If true, the arguments for each method call
+ * will be displayed.
+ * - `start` - The stack frame to start generating a trace from. Defaults to 0
+ *
+ * @param array|\Throwable $backtrace Trace as array or an exception object.
+ * @param array $options Format for outputting stack trace.
+ * @return string|array Formatted stack trace.
+ * @link https://book.cakephp.org/4/en/development/debugging.html#generating-stack-traces
+ */
+ public static function formatTrace($backtrace, array $options = [])
+ {
+ if ($backtrace instanceof Throwable) {
+ $backtrace = $backtrace->getTrace();
+ }
+ $self = Debugger::getInstance();
+ $defaults = [
+ 'depth' => 999,
+ 'format' => $self->_outputFormat,
+ 'args' => false,
+ 'start' => 0,
+ 'scope' => null,
+ 'exclude' => ['call_user_func_array', 'trigger_error'],
+ ];
+ $options = Hash::merge($defaults, $options);
+
+ $count = count($backtrace);
+ $back = [];
+
+ $_trace = [
+ 'line' => '??',
+ 'file' => '[internal]',
+ 'class' => null,
+ 'function' => '[main]',
+ ];
+
+ for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
+ $trace = $backtrace[$i] + ['file' => '[internal]', 'line' => '??'];
+ $signature = $reference = '[main]';
+
+ if (isset($backtrace[$i + 1])) {
+ $next = $backtrace[$i + 1] + $_trace;
+ $signature = $reference = $next['function'];
+
+ if (!empty($next['class'])) {
+ $signature = $next['class'] . '::' . $next['function'];
+ $reference = $signature . '(';
+ if ($options['args'] && isset($next['args'])) {
+ $args = [];
+ foreach ($next['args'] as $arg) {
+ $args[] = Debugger::exportVar($arg);
+ }
+ $reference .= implode(', ', $args);
+ }
+ $reference .= ')';
+ }
+ }
+ if (in_array($signature, $options['exclude'], true)) {
+ continue;
+ }
+ if ($options['format'] === 'points' && $trace['file'] !== '[internal]') {
+ $back[] = ['file' => $trace['file'], 'line' => $trace['line']];
+ } elseif ($options['format'] === 'array') {
+ $back[] = $trace;
+ } else {
+ if (isset($self->_templates[$options['format']]['traceLine'])) {
+ $tpl = $self->_templates[$options['format']]['traceLine'];
+ } else {
+ $tpl = $self->_templates['base']['traceLine'];
+ }
+ $trace['path'] = static::trimPath($trace['file']);
+ $trace['reference'] = $reference;
+ unset($trace['object'], $trace['args']);
+ $back[] = Text::insert($tpl, $trace, ['before' => '{:', 'after' => '}']);
+ }
+ }
+
+ if ($options['format'] === 'array' || $options['format'] === 'points') {
+ return $back;
+ }
+
+ return implode("\n", $back);
+ }
+
+ /**
+ * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core
+ * path with 'CORE'.
+ *
+ * @param string $path Path to shorten.
+ * @return string Normalized path
+ */
+ public static function trimPath(string $path): string
+ {
+ if (defined('APP') && strpos($path, APP) === 0) {
+ return str_replace(APP, 'APP/', $path);
+ }
+ if (defined('CAKE_CORE_INCLUDE_PATH') && strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) {
+ return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path);
+ }
+ if (defined('ROOT') && strpos($path, ROOT) === 0) {
+ return str_replace(ROOT, 'ROOT', $path);
+ }
+
+ return $path;
+ }
+
+ /**
+ * Grabs an excerpt from a file and highlights a given line of code.
+ *
+ * Usage:
+ *
+ * ```
+ * Debugger::excerpt('/path/to/file', 100, 4);
+ * ```
+ *
+ * The above would return an array of 8 items. The 4th item would be the provided line,
+ * and would be wrapped in ``. All of the lines
+ * are processed with highlight_string() as well, so they have basic PHP syntax highlighting
+ * applied.
+ *
+ * @param string $file Absolute path to a PHP file.
+ * @param int $line Line number to highlight.
+ * @param int $context Number of lines of context to extract above and below $line.
+ * @return array Set of lines highlighted
+ * @see https://secure.php.net/highlight_string
+ * @link https://book.cakephp.org/4/en/development/debugging.html#getting-an-excerpt-from-a-file
+ */
+ public static function excerpt(string $file, int $line, int $context = 2): array
+ {
+ $lines = [];
+ if (!file_exists($file)) {
+ return [];
+ }
+ $data = file_get_contents($file);
+ if (empty($data)) {
+ return $lines;
+ }
+ if (strpos($data, "\n") !== false) {
+ $data = explode("\n", $data);
+ }
+ $line--;
+ if (!isset($data[$line])) {
+ return $lines;
+ }
+ for ($i = $line - $context; $i < $line + $context + 1; $i++) {
+ if (!isset($data[$i])) {
+ continue;
+ }
+ $string = str_replace(["\r\n", "\n"], '', static::_highlight($data[$i]));
+ if ($i === $line) {
+ $lines[] = '' . $string . '';
+ } else {
+ $lines[] = $string;
+ }
+ }
+
+ return $lines;
+ }
+
+ /**
+ * Wraps the highlight_string function in case the server API does not
+ * implement the function as it is the case of the HipHop interpreter
+ *
+ * @param string $str The string to convert.
+ * @return string
+ */
+ protected static function _highlight(string $str): string
+ {
+ if (function_exists('hphp_log') || function_exists('hphp_gettid')) {
+ return htmlentities($str);
+ }
+ $added = false;
+ if (strpos($str, '', '<?php
'],
+ '',
+ $highlight
+ );
+ }
+
+ return $highlight;
+ }
+
+ /**
+ * Get the configured export formatter or infer one based on the environment.
+ *
+ * @return \Cake\Error\Debug\FormatterInterface
+ * @unstable This method is not stable and may change in the future.
+ * @since 4.1.0
+ */
+ public function getExportFormatter(): FormatterInterface
+ {
+ $instance = static::getInstance();
+ $class = $instance->getConfig('exportFormatter');
+ if (!$class) {
+ if (ConsoleFormatter::environmentMatches()) {
+ $class = ConsoleFormatter::class;
+ } elseif (HtmlFormatter::environmentMatches()) {
+ $class = HtmlFormatter::class;
+ } else {
+ $class = TextFormatter::class;
+ }
+ }
+ $instance = new $class();
+ if (!$instance instanceof FormatterInterface) {
+ throw new RuntimeException(
+ "The `{$class}` formatter does not implement " . FormatterInterface::class
+ );
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Converts a variable to a string for debug output.
+ *
+ * *Note:* The following keys will have their contents
+ * replaced with `*****`:
+ *
+ * - password
+ * - login
+ * - host
+ * - database
+ * - port
+ * - prefix
+ * - schema
+ *
+ * This is done to protect database credentials, which could be accidentally
+ * shown in an error message if CakePHP is deployed in development mode.
+ *
+ * @param mixed $var Variable to convert.
+ * @param int $maxDepth The depth to output to. Defaults to 3.
+ * @return string Variable as a formatted string
+ */
+ public static function exportVar($var, int $maxDepth = 3): string
+ {
+ $context = new DebugContext($maxDepth);
+ $node = static::export($var, $context);
+
+ return static::getInstance()->getExportFormatter()->dump($node);
+ }
+
+ /**
+ * Convert the variable to the internal node tree.
+ *
+ * The node tree can be manipulated and serialized more easily
+ * than many object graphs can.
+ *
+ * @param mixed $var Variable to convert.
+ * @param int $maxDepth The depth to generate nodes to. Defaults to 3.
+ * @return \Cake\Error\Debug\NodeInterface The root node of the tree.
+ */
+ public static function exportVarAsNodes($var, int $maxDepth = 3): NodeInterface
+ {
+ return static::export($var, new DebugContext($maxDepth));
+ }
+
+ /**
+ * Protected export function used to keep track of indentation and recursion.
+ *
+ * @param mixed $var The variable to dump.
+ * @param \Cake\Error\Debug\DebugContext $context Dump context
+ * @return \Cake\Error\Debug\NodeInterface The dumped variable.
+ */
+ protected static function export($var, DebugContext $context): NodeInterface
+ {
+ $type = static::getType($var);
+ switch ($type) {
+ case 'float':
+ case 'string':
+ case 'resource':
+ case 'resource (closed)':
+ case 'null':
+ return new ScalarNode($type, $var);
+ case 'boolean':
+ return new ScalarNode('bool', $var);
+ case 'integer':
+ return new ScalarNode('int', $var);
+ case 'array':
+ return static::exportArray($var, $context->withAddedDepth());
+ case 'unknown':
+ return new SpecialNode('(unknown)');
+ default:
+ return static::exportObject($var, $context->withAddedDepth());
+ }
+ }
+
+ /**
+ * Export an array type object. Filters out keys used in datasource configuration.
+ *
+ * The following keys are replaced with ***'s
+ *
+ * - password
+ * - login
+ * - host
+ * - database
+ * - port
+ * - prefix
+ * - schema
+ *
+ * @param array $var The array to export.
+ * @param \Cake\Error\Debug\DebugContext $context The current dump context.
+ * @return \Cake\Error\Debug\ArrayNode Exported array.
+ */
+ protected static function exportArray(array $var, DebugContext $context): ArrayNode
+ {
+ $items = [];
+
+ $remaining = $context->remainingDepth();
+ if ($remaining >= 0) {
+ $outputMask = static::outputMask();
+ foreach ($var as $key => $val) {
+ if (array_key_exists($key, $outputMask)) {
+ $node = new ScalarNode('string', $outputMask[$key]);
+ } elseif ($val !== $var) {
+ // Dump all the items without increasing depth.
+ $node = static::export($val, $context);
+ } else {
+ // Likely recursion, so we increase depth.
+ $node = static::export($val, $context->withAddedDepth());
+ }
+ $items[] = new ArrayItemNode(static::export($key, $context), $node);
+ }
+ } else {
+ $items[] = new ArrayItemNode(
+ new ScalarNode('string', ''),
+ new SpecialNode('[maximum depth reached]')
+ );
+ }
+
+ return new ArrayNode($items);
+ }
+
+ /**
+ * Handles object to node conversion.
+ *
+ * @param object $var Object to convert.
+ * @param \Cake\Error\Debug\DebugContext $context The dump context.
+ * @return \Cake\Error\Debug\NodeInterface
+ * @see \Cake\Error\Debugger::exportVar()
+ */
+ protected static function exportObject(object $var, DebugContext $context): NodeInterface
+ {
+ $isRef = $context->hasReference($var);
+ $refNum = $context->getReferenceId($var);
+
+ $className = get_class($var);
+ if ($isRef) {
+ return new ReferenceNode($className, $refNum);
+ }
+ $node = new ClassNode($className, $refNum);
+
+ $remaining = $context->remainingDepth();
+ if ($remaining > 0) {
+ if (method_exists($var, '__debugInfo')) {
+ try {
+ foreach ($var->__debugInfo() as $key => $val) {
+ $node->addProperty(new PropertyNode("'{$key}'", null, static::export($val, $context)));
+ }
+
+ return $node;
+ } catch (Exception $e) {
+ return new SpecialNode("(unable to export object: {$e->getMessage()})");
+ }
+ }
+
+ $outputMask = static::outputMask();
+ $objectVars = get_object_vars($var);
+ foreach ($objectVars as $key => $value) {
+ if (array_key_exists($key, $outputMask)) {
+ $value = $outputMask[$key];
+ }
+ /** @psalm-suppress RedundantCast */
+ $node->addProperty(
+ new PropertyNode((string)$key, 'public', static::export($value, $context->withAddedDepth()))
+ );
+ }
+
+ $ref = new ReflectionObject($var);
+
+ $filters = [
+ ReflectionProperty::IS_PROTECTED => 'protected',
+ ReflectionProperty::IS_PRIVATE => 'private',
+ ];
+ foreach ($filters as $filter => $visibility) {
+ $reflectionProperties = $ref->getProperties($filter);
+ foreach ($reflectionProperties as $reflectionProperty) {
+ $reflectionProperty->setAccessible(true);
+
+ if (
+ method_exists($reflectionProperty, 'isInitialized') &&
+ !$reflectionProperty->isInitialized($var)
+ ) {
+ $value = new SpecialNode('[uninitialized]');
+ } else {
+ $value = static::export($reflectionProperty->getValue($var), $context->withAddedDepth());
+ }
+ $node->addProperty(
+ new PropertyNode(
+ $reflectionProperty->getName(),
+ $visibility,
+ $value
+ )
+ );
+ }
+ }
+ }
+
+ return $node;
+ }
+
+ /**
+ * Get the output format for Debugger error rendering.
+ *
+ * @return string Returns the current format when getting.
+ */
+ public static function getOutputFormat(): string
+ {
+ return Debugger::getInstance()->_outputFormat;
+ }
+
+ /**
+ * Set the output format for Debugger error rendering.
+ *
+ * @param string $format The format you want errors to be output as.
+ * @return void
+ * @throws \InvalidArgumentException When choosing a format that doesn't exist.
+ */
+ public static function setOutputFormat(string $format): void
+ {
+ $self = Debugger::getInstance();
+
+ if (!isset($self->_templates[$format])) {
+ throw new InvalidArgumentException('Invalid Debugger output format.');
+ }
+ $self->_outputFormat = $format;
+ }
+
+ /**
+ * Add an output format or update a format in Debugger.
+ *
+ * ```
+ * Debugger::addFormat('custom', $data);
+ * ```
+ *
+ * Where $data is an array of strings that use Text::insert() variable
+ * replacement. The template vars should be in a `{:id}` style.
+ * An error formatter can have the following keys:
+ *
+ * - 'error' - Used for the container for the error message. Gets the following template
+ * variables: `id`, `error`, `code`, `description`, `path`, `line`, `links`, `info`
+ * - 'info' - A combination of `code`, `context` and `trace`. Will be set with
+ * the contents of the other template keys.
+ * - 'trace' - The container for a stack trace. Gets the following template
+ * variables: `trace`
+ * - 'context' - The container element for the context variables.
+ * Gets the following templates: `id`, `context`
+ * - 'links' - An array of HTML links that are used for creating links to other resources.
+ * Typically this is used to create javascript links to open other sections.
+ * Link keys, are: `code`, `context`, `help`. See the JS output format for an
+ * example.
+ * - 'traceLine' - Used for creating lines in the stacktrace. Gets the following
+ * template variables: `reference`, `path`, `line`
+ *
+ * Alternatively if you want to use a custom callback to do all the formatting, you can use
+ * the callback key, and provide a callable:
+ *
+ * ```
+ * Debugger::addFormat('custom', ['callback' => [$foo, 'outputError']];
+ * ```
+ *
+ * The callback can expect two parameters. The first is an array of all
+ * the error data. The second contains the formatted strings generated using
+ * the other template strings. Keys like `info`, `links`, `code`, `context` and `trace`
+ * will be present depending on the other templates in the format type.
+ *
+ * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for
+ * straight HTML output, or 'txt' for unformatted text.
+ * @param array $strings Template strings, or a callback to be used for the output format.
+ * @return array The resulting format string set.
+ */
+ public static function addFormat(string $format, array $strings): array
+ {
+ $self = Debugger::getInstance();
+ if (isset($self->_templates[$format])) {
+ if (isset($strings['links'])) {
+ $self->_templates[$format]['links'] = array_merge(
+ $self->_templates[$format]['links'],
+ $strings['links']
+ );
+ unset($strings['links']);
+ }
+ $self->_templates[$format] = $strings + $self->_templates[$format];
+ } else {
+ $self->_templates[$format] = $strings;
+ }
+
+ return $self->_templates[$format];
+ }
+
+ /**
+ * Takes a processed array of data from an error and displays it in the chosen format.
+ *
+ * @param array $data Data to output.
+ * @return void
+ */
+ public function outputError(array $data): void
+ {
+ $defaults = [
+ 'level' => 0,
+ 'error' => 0,
+ 'code' => 0,
+ 'description' => '',
+ 'file' => '',
+ 'line' => 0,
+ 'context' => [],
+ 'start' => 2,
+ ];
+ $data += $defaults;
+
+ $files = static::trace(['start' => $data['start'], 'format' => 'points']);
+ $code = '';
+ $file = null;
+ if (isset($files[0]['file'])) {
+ $file = $files[0];
+ } elseif (isset($files[1]['file'])) {
+ $file = $files[1];
+ }
+ if ($file) {
+ $code = static::excerpt($file['file'], $file['line'], 1);
+ }
+ $trace = static::trace(['start' => $data['start'], 'depth' => '20']);
+ $insertOpts = ['before' => '{:', 'after' => '}'];
+ $context = [];
+ $links = [];
+ $info = '';
+
+ foreach ((array)$data['context'] as $var => $value) {
+ $context[] = "\${$var} = " . static::exportVar($value, 3);
+ }
+
+ switch ($this->_outputFormat) {
+ case false:
+ $this->_data[] = compact('context', 'trace') + $data;
+
+ return;
+ case 'log':
+ static::log(compact('context', 'trace') + $data);
+
+ return;
+ }
+
+ $data['trace'] = $trace;
+ $data['id'] = 'cakeErr' . uniqid();
+ $tpl = $this->_templates[$this->_outputFormat] + $this->_templates['base'];
+
+ if (isset($tpl['links'])) {
+ foreach ($tpl['links'] as $key => $val) {
+ $links[$key] = Text::insert($val, $data, $insertOpts);
+ }
+ }
+
+ if (!empty($tpl['escapeContext'])) {
+ $data['description'] = h($data['description']);
+ }
+
+ $infoData = compact('code', 'context', 'trace');
+ foreach ($infoData as $key => $value) {
+ if (empty($value) || !isset($tpl[$key])) {
+ continue;
+ }
+ if (is_array($value)) {
+ $value = implode("\n", $value);
+ }
+ $info .= Text::insert($tpl[$key], [$key => $value] + $data, $insertOpts);
+ }
+ $links = implode(' ', $links);
+
+ if (isset($tpl['callback']) && is_callable($tpl['callback'])) {
+ $tpl['callback']($data, compact('links', 'info'));
+
+ return;
+ }
+ echo Text::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
+ }
+
+ /**
+ * Get the type of the given variable. Will return the class name
+ * for objects.
+ *
+ * @param mixed $var The variable to get the type of.
+ * @return string The type of variable.
+ */
+ public static function getType($var): string
+ {
+ $type = getTypeName($var);
+
+ if ($type === 'NULL') {
+ return 'null';
+ }
+
+ if ($type === 'double') {
+ return 'float';
+ }
+
+ if ($type === 'unknown type') {
+ return 'unknown';
+ }
+
+ return $type;
+ }
+
+ /**
+ * Prints out debug information about given variable.
+ *
+ * @param mixed $var Variable to show debug information for.
+ * @param array $location If contains keys "file" and "line" their values will
+ * be used to show location info.
+ * @param bool|null $showHtml If set to true, the method prints the debug
+ * data encoded as HTML. If false, plain text formatting will be used.
+ * If null, the format will be chosen based on the configured exportFormatter, or
+ * environment conditions.
+ * @return void
+ */
+ public static function printVar($var, array $location = [], ?bool $showHtml = null): void
+ {
+ $location += ['file' => null, 'line' => null];
+ if ($location['file']) {
+ $location['file'] = static::trimPath((string)$location['file']);
+ }
+
+ $debugger = static::getInstance();
+ $restore = null;
+ if ($showHtml !== null) {
+ $restore = $debugger->getConfig('exportFormatter');
+ $debugger->setConfig('exportFormatter', $showHtml ? HtmlFormatter::class : TextFormatter::class);
+ }
+ $contents = static::exportVar($var, 25);
+ $formatter = $debugger->getExportFormatter();
+
+ if ($restore) {
+ $debugger->setConfig('exportFormatter', $restore);
+ }
+ echo $formatter->formatWrapper($contents, $location);
+ }
+
+ /**
+ * Format an exception message to be HTML formatted.
+ *
+ * Does the following formatting operations:
+ *
+ * - HTML escape the message.
+ * - Convert `bool` into `bool`
+ * - Convert newlines into `
`
+ *
+ * @param string $message The string message to format.
+ * @return string Formatted message.
+ */
+ public static function formatHtmlMessage(string $message): string
+ {
+ $message = h($message);
+ $message = preg_replace('/`([^`]+)`/', '$1', $message);
+ $message = nl2br($message);
+
+ return $message;
+ }
+
+ /**
+ * Verifies that the application's salt and cipher seed value has been changed from the default value.
+ *
+ * @return void
+ */
+ public static function checkSecurityKeys(): void
+ {
+ $salt = Security::getSalt();
+ if ($salt === '__SALT__' || strlen($salt) < 32) {
+ trigger_error(
+ 'Please change the value of `Security.salt` in `ROOT/config/app_local.php` ' .
+ 'to a random value of at least 32 characters.',
+ E_USER_NOTICE
+ );
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/ErrorHandler.php b/app/vendor/cakephp/cakephp/src/Error/ErrorHandler.php
new file mode 100644
index 000000000..b78de70b5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/ErrorHandler.php
@@ -0,0 +1,216 @@
+ ExceptionRenderer::class,
+ ];
+
+ $this->setConfig($config);
+ }
+
+ /**
+ * Display an error.
+ *
+ * Template method of BaseErrorHandler.
+ *
+ * @param array $error An array of error data.
+ * @param bool $debug Whether or not the app is in debug mode.
+ * @return void
+ */
+ protected function _displayError(array $error, bool $debug): void
+ {
+ if (!$debug) {
+ return;
+ }
+ Debugger::getInstance()->outputError($error);
+ }
+
+ /**
+ * Displays an exception response body.
+ *
+ * @param \Throwable $exception The exception to display.
+ * @return void
+ * @throws \Exception When the chosen exception renderer is invalid.
+ */
+ protected function _displayException(Throwable $exception): void
+ {
+ try {
+ $renderer = $this->getRenderer(
+ $exception,
+ Router::getRequest()
+ );
+ $response = $renderer->render();
+ $this->_sendResponse($response);
+ } catch (Throwable $exception) {
+ $this->_logInternalError($exception);
+ }
+ }
+
+ /**
+ * Get a renderer instance.
+ *
+ * @param \Throwable $exception The exception being rendered.
+ * @param \Psr\Http\Message\ServerRequestInterface|null $request The request.
+ * @return \Cake\Error\ExceptionRendererInterface The exception renderer.
+ * @throws \RuntimeException When the renderer class cannot be found.
+ */
+ public function getRenderer(
+ Throwable $exception,
+ ?ServerRequestInterface $request = null
+ ): ExceptionRendererInterface {
+ $renderer = $this->_config['exceptionRenderer'];
+
+ if (is_string($renderer)) {
+ /** @var class-string<\Cake\Error\ExceptionRendererInterface>|null $class */
+ $class = App::className($renderer, 'Error');
+ if (!$class) {
+ throw new RuntimeException(sprintf(
+ "The '%s' renderer class could not be found.",
+ $renderer
+ ));
+ }
+
+ return new $class($exception, $request);
+ }
+
+ /** @var callable $factory */
+ $factory = $renderer;
+
+ return $factory($exception, $request);
+ }
+
+ /**
+ * Log internal errors.
+ *
+ * @param \Throwable $exception Exception.
+ * @return void
+ */
+ protected function _logInternalError(Throwable $exception): void
+ {
+ // Disable trace for internal errors.
+ $this->_config['trace'] = false;
+ $message = sprintf(
+ "[%s] %s (%s:%s)\n%s", // Keeping same message format
+ get_class($exception),
+ $exception->getMessage(),
+ $exception->getFile(),
+ $exception->getLine(),
+ $exception->getTraceAsString()
+ );
+ trigger_error($message, E_USER_ERROR);
+ }
+
+ /**
+ * Method that can be easily stubbed in testing.
+ *
+ * @param string|\Cake\Http\Response $response Either the message or response object.
+ * @return void
+ */
+ protected function _sendResponse($response): void
+ {
+ if (is_string($response)) {
+ echo $response;
+
+ return;
+ }
+
+ $emitter = new ResponseEmitter();
+ $emitter->emit($response);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/ErrorLogger.php b/app/vendor/cakephp/cakephp/src/Error/ErrorLogger.php
new file mode 100644
index 000000000..21285bd5d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/ErrorLogger.php
@@ -0,0 +1,165 @@
+ [],
+ 'trace' => false,
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param array $config Config array.
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function logMessage($level, string $message, array $context = []): bool
+ {
+ if (!empty($context['request'])) {
+ $message .= $this->getRequestContext($context['request']);
+ }
+ if (!empty($context['trace'])) {
+ $message .= "\nTrace:\n" . $context['trace'] . "\n";
+ }
+
+ return Log::write($level, $message);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function log(Throwable $exception, ?ServerRequestInterface $request = null): bool
+ {
+ foreach ($this->getConfig('skipLog') as $class) {
+ if ($exception instanceof $class) {
+ return false;
+ }
+ }
+
+ $message = $this->getMessage($exception);
+
+ if ($request !== null) {
+ $message .= $this->getRequestContext($request);
+ }
+
+ $message .= "\n\n";
+
+ return Log::error($message);
+ }
+
+ /**
+ * Generate the message for the exception
+ *
+ * @param \Throwable $exception The exception to log a message for.
+ * @param bool $isPrevious False for original exception, true for previous
+ * @return string Error message
+ */
+ protected function getMessage(Throwable $exception, bool $isPrevious = false): string
+ {
+ $message = sprintf(
+ '%s[%s] %s in %s on line %s',
+ $isPrevious ? "\nCaused by: " : '',
+ get_class($exception),
+ $exception->getMessage(),
+ $exception->getFile(),
+ $exception->getLine()
+ );
+ $debug = Configure::read('debug');
+
+ if ($debug && $exception instanceof CakeException) {
+ $attributes = $exception->getAttributes();
+ if ($attributes) {
+ $message .= "\nException Attributes: " . var_export($exception->getAttributes(), true);
+ }
+ }
+
+ if ($this->getConfig('trace')) {
+ /** @var array $trace */
+ $trace = Debugger::formatTrace($exception, ['format' => 'points']);
+ $message .= "\nStack Trace:\n";
+ foreach ($trace as $line) {
+ if (is_string($line)) {
+ $message .= '- ' . $line;
+ } else {
+ $message .= "- {$line['file']}:{$line['line']}\n";
+ }
+ }
+ }
+
+ $previous = $exception->getPrevious();
+ if ($previous) {
+ $message .= $this->getMessage($previous, true);
+ }
+
+ return $message;
+ }
+
+ /**
+ * Get the request context for an error/exception trace.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to read from.
+ * @return string
+ */
+ public function getRequestContext(ServerRequestInterface $request): string
+ {
+ $message = "\nRequest URL: " . $request->getRequestTarget();
+
+ $referer = $request->getHeaderLine('Referer');
+ if ($referer) {
+ $message .= "\nReferer URL: " . $referer;
+ }
+
+ if (method_exists($request, 'clientIp')) {
+ $clientIp = $request->clientIp();
+ if ($clientIp && $clientIp !== '::1') {
+ $message .= "\nClient IP: " . $clientIp;
+ }
+ }
+
+ return $message;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/ErrorLoggerInterface.php b/app/vendor/cakephp/cakephp/src/Error/ErrorLoggerInterface.php
new file mode 100644
index 000000000..b8ee57c23
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/ErrorLoggerInterface.php
@@ -0,0 +1,51 @@
+, int>
+ */
+ protected $exceptionHttpCodes = [
+ // Controller exceptions
+ MissingActionException::class => 404,
+ // Datasource exceptions
+ PageOutOfBoundsException::class => 404,
+ RecordNotFoundException::class => 404,
+ // Http exceptions
+ MissingControllerException::class => 404,
+ // Routing exceptions
+ MissingRouteException::class => 404,
+ ];
+
+ /**
+ * Creates the controller to perform rendering on the error response.
+ *
+ * @param \Throwable $exception Exception.
+ * @param \Cake\Http\ServerRequest $request The request if this is set it will be used
+ * instead of creating a new one.
+ */
+ public function __construct(Throwable $exception, ?ServerRequest $request = null)
+ {
+ $this->error = $exception;
+ $this->request = $request;
+ $this->controller = $this->_getController();
+ }
+
+ /**
+ * Get the controller instance to handle the exception.
+ * Override this method in subclasses to customize the controller used.
+ * This method returns the built in `ErrorController` normally, or if an error is repeated
+ * a bare controller will be used.
+ *
+ * @return \Cake\Controller\Controller
+ * @triggers Controller.startup $controller
+ */
+ protected function _getController(): Controller
+ {
+ $request = $this->request;
+ $routerRequest = Router::getRequest();
+ // Fallback to the request in the router or make a new one from
+ // $_SERVER
+ if ($request === null) {
+ $request = $routerRequest ?: ServerRequestFactory::fromGlobals();
+ }
+
+ // If the current request doesn't have routing data, but we
+ // found a request in the router context copy the params over
+ if ($request->getParam('controller') === null && $routerRequest !== null) {
+ $request = $request->withAttribute('params', $routerRequest->getAttribute('params'));
+ }
+
+ $errorOccured = false;
+ try {
+ $params = $request->getAttribute('params');
+ $params['controller'] = 'Error';
+
+ $factory = new ControllerFactory(new Container());
+ $class = $factory->getControllerClass($request->withAttribute('params', $params));
+
+ if (!$class) {
+ /** @var string $class */
+ $class = App::className('Error', 'Controller', 'Controller');
+ }
+
+ /** @var \Cake\Controller\Controller $controller */
+ $controller = new $class($request);
+ $controller->startupProcess();
+ } catch (Throwable $e) {
+ $errorOccured = true;
+ }
+
+ if (!isset($controller)) {
+ return new Controller($request);
+ }
+
+ // Retry RequestHandler, as another aspect of startupProcess()
+ // could have failed. Ignore any exceptions out of startup, as
+ // there could be userland input data parsers.
+ if ($errorOccured && isset($controller->RequestHandler)) {
+ try {
+ $event = new Event('Controller.startup', $controller);
+ $controller->RequestHandler->startup($event);
+ } catch (Throwable $e) {
+ }
+ }
+
+ return $controller;
+ }
+
+ /**
+ * Clear output buffers so error pages display properly.
+ *
+ * @return void
+ */
+ protected function clearOutput(): void
+ {
+ if (in_array(PHP_SAPI, ['cli', 'phpdbg'])) {
+ return;
+ }
+ while (ob_get_level()) {
+ ob_end_clean();
+ }
+ }
+
+ /**
+ * Renders the response for the exception.
+ *
+ * @return \Cake\Http\Response The response to be sent.
+ */
+ public function render(): ResponseInterface
+ {
+ $exception = $this->error;
+ $code = $this->getHttpCode($exception);
+ $method = $this->_method($exception);
+ $template = $this->_template($exception, $method, $code);
+ $this->clearOutput();
+
+ if (method_exists($this, $method)) {
+ return $this->_customMethod($method, $exception);
+ }
+
+ $message = $this->_message($exception, $code);
+ $url = $this->controller->getRequest()->getRequestTarget();
+ $response = $this->controller->getResponse();
+
+ if ($exception instanceof CakeException) {
+ /** @psalm-suppress DeprecatedMethod */
+ foreach ((array)$exception->responseHeader() as $key => $value) {
+ $response = $response->withHeader($key, $value);
+ }
+ }
+ if ($exception instanceof HttpException) {
+ foreach ($exception->getHeaders() as $name => $value) {
+ $response = $response->withHeader($name, $value);
+ }
+ }
+ $response = $response->withStatus($code);
+
+ $viewVars = [
+ 'message' => $message,
+ 'url' => h($url),
+ 'error' => $exception,
+ 'code' => $code,
+ ];
+ $serialize = ['message', 'url', 'code'];
+
+ $isDebug = Configure::read('debug');
+ if ($isDebug) {
+ $trace = (array)Debugger::formatTrace($exception->getTrace(), [
+ 'format' => 'array',
+ 'args' => false,
+ ]);
+ $origin = [
+ 'file' => $exception->getFile() ?: 'null',
+ 'line' => $exception->getLine() ?: 'null',
+ ];
+ // Traces don't include the origin file/line.
+ array_unshift($trace, $origin);
+ $viewVars['trace'] = $trace;
+ $viewVars += $origin;
+ $serialize[] = 'file';
+ $serialize[] = 'line';
+ }
+ $this->controller->set($viewVars);
+ $this->controller->viewBuilder()->setOption('serialize', $serialize);
+
+ if ($exception instanceof CakeException && $isDebug) {
+ $this->controller->set($exception->getAttributes());
+ }
+ $this->controller->setResponse($response);
+
+ return $this->_outputMessage($template);
+ }
+
+ /**
+ * Render a custom error method/template.
+ *
+ * @param string $method The method name to invoke.
+ * @param \Throwable $exception The exception to render.
+ * @return \Cake\Http\Response The response to send.
+ */
+ protected function _customMethod(string $method, Throwable $exception): Response
+ {
+ $result = $this->{$method}($exception);
+ $this->_shutdown();
+ if (is_string($result)) {
+ $result = $this->controller->getResponse()->withStringBody($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get method name
+ *
+ * @param \Throwable $exception Exception instance.
+ * @return string
+ */
+ protected function _method(Throwable $exception): string
+ {
+ [, $baseClass] = namespaceSplit(get_class($exception));
+
+ if (substr($baseClass, -9) === 'Exception') {
+ $baseClass = substr($baseClass, 0, -9);
+ }
+
+ // $baseClass would be an empty string if the exception class is \Exception.
+ $method = $baseClass === '' ? 'error500' : Inflector::variable($baseClass);
+
+ return $this->method = $method;
+ }
+
+ /**
+ * Get error message.
+ *
+ * @param \Throwable $exception Exception.
+ * @param int $code Error code.
+ * @return string Error message
+ */
+ protected function _message(Throwable $exception, int $code): string
+ {
+ $message = $exception->getMessage();
+
+ if (
+ !Configure::read('debug') &&
+ !($exception instanceof HttpException)
+ ) {
+ if ($code < 500) {
+ $message = __d('cake', 'Not Found');
+ } else {
+ $message = __d('cake', 'An Internal Error Has Occurred.');
+ }
+ }
+
+ return $message;
+ }
+
+ /**
+ * Get template for rendering exception info.
+ *
+ * @param \Throwable $exception Exception instance.
+ * @param string $method Method name.
+ * @param int $code Error code.
+ * @return string Template name
+ */
+ protected function _template(Throwable $exception, string $method, int $code): string
+ {
+ if ($exception instanceof HttpException || !Configure::read('debug')) {
+ return $this->template = $code < 500 ? 'error400' : 'error500';
+ }
+
+ if ($exception instanceof PDOException) {
+ return $this->template = 'pdo_error';
+ }
+
+ return $this->template = $method;
+ }
+
+ /**
+ * Gets the appropriate http status code for exception.
+ *
+ * @param \Throwable $exception Exception.
+ * @return int A valid HTTP status code.
+ */
+ protected function getHttpCode(Throwable $exception): int
+ {
+ if ($exception instanceof HttpException) {
+ return $exception->getCode();
+ }
+
+ return $this->exceptionHttpCodes[get_class($exception)] ?? 500;
+ }
+
+ /**
+ * Generate the response using the controller object.
+ *
+ * @param string $template The template to render.
+ * @return \Cake\Http\Response A response object that can be sent.
+ */
+ protected function _outputMessage(string $template): Response
+ {
+ try {
+ $this->controller->render($template);
+
+ return $this->_shutdown();
+ } catch (MissingTemplateException $e) {
+ $attributes = $e->getAttributes();
+ if (
+ $e instanceof MissingLayoutException ||
+ (
+ isset($attributes['file']) &&
+ strpos($attributes['file'], 'error500') !== false
+ )
+ ) {
+ return $this->_outputMessageSafe('error500');
+ }
+
+ return $this->_outputMessage('error500');
+ } catch (MissingPluginException $e) {
+ $attributes = $e->getAttributes();
+ if (isset($attributes['plugin']) && $attributes['plugin'] === $this->controller->getPlugin()) {
+ $this->controller->setPlugin(null);
+ }
+
+ return $this->_outputMessageSafe('error500');
+ } catch (Throwable $e) {
+ return $this->_outputMessageSafe('error500');
+ }
+ }
+
+ /**
+ * A safer way to render error messages, replaces all helpers, with basics
+ * and doesn't call component methods.
+ *
+ * @param string $template The template to render.
+ * @return \Cake\Http\Response A response object that can be sent.
+ */
+ protected function _outputMessageSafe(string $template): Response
+ {
+ $builder = $this->controller->viewBuilder();
+ $builder
+ ->setHelpers([], false)
+ ->setLayoutPath('')
+ ->setTemplatePath('Error');
+ $view = $this->controller->createView('View');
+
+ $response = $this->controller->getResponse()
+ ->withType('html')
+ ->withStringBody($view->render($template, 'error'));
+ $this->controller->setResponse($response);
+
+ return $response;
+ }
+
+ /**
+ * Run the shutdown events.
+ *
+ * Triggers the afterFilter and afterDispatch events.
+ *
+ * @return \Cake\Http\Response The response to serve.
+ */
+ protected function _shutdown(): Response
+ {
+ $this->controller->dispatchEvent('Controller.shutdown');
+
+ return $this->controller->getResponse();
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'error' => $this->error,
+ 'request' => $this->request,
+ 'controller' => $this->controller,
+ 'template' => $this->template,
+ 'method' => $this->method,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/ExceptionRendererInterface.php b/app/vendor/cakephp/cakephp/src/Error/ExceptionRendererInterface.php
new file mode 100644
index 000000000..103e244ac
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/ExceptionRendererInterface.php
@@ -0,0 +1,32 @@
+file = $file;
+ }
+ if ($line) {
+ $this->line = $line;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php b/app/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php
new file mode 100644
index 000000000..8f2ea7967
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php
@@ -0,0 +1,199 @@
+ ['Cake\Error\NotFoundException', 'Cake\Error\UnauthorizedException']
+ * ```
+ *
+ * - `trace` Should error logs include stack traces?
+ * - `exceptionRenderer` The renderer instance or class name to use or a callable factory
+ * which returns a \Cake\Error\ExceptionRendererInterface instance.
+ * Defaults to \Cake\Error\ExceptionRenderer
+ *
+ * @var array
+ */
+ protected $_defaultConfig = [
+ 'skipLog' => [],
+ 'log' => true,
+ 'trace' => false,
+ 'exceptionRenderer' => ExceptionRenderer::class,
+ ];
+
+ /**
+ * Error handler instance.
+ *
+ * @var \Cake\Error\ErrorHandler|null
+ */
+ protected $errorHandler;
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\Error\ErrorHandler|array $errorHandler The error handler instance
+ * or config array.
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($errorHandler = [])
+ {
+ if (func_num_args() > 1) {
+ deprecationWarning(
+ 'The signature of ErrorHandlerMiddleware::__construct() has changed. '
+ . 'Pass the config array as 1st argument instead.'
+ );
+
+ $errorHandler = func_get_arg(1);
+ }
+
+ if (PHP_VERSION_ID >= 70400 && Configure::read('debug')) {
+ ini_set('zend.exception_ignore_args', '0');
+ }
+
+ if (is_array($errorHandler)) {
+ $this->setConfig($errorHandler);
+
+ return;
+ }
+
+ if (!$errorHandler instanceof ErrorHandler) {
+ throw new InvalidArgumentException(sprintf(
+ '$errorHandler argument must be a config array or ErrorHandler instance. Got `%s` instead.',
+ getTypeName($errorHandler)
+ ));
+ }
+
+ $this->errorHandler = $errorHandler;
+ }
+
+ /**
+ * Wrap the remaining middleware with error handling.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ try {
+ return $handler->handle($request);
+ } catch (RedirectException $exception) {
+ return $this->handleRedirect($exception);
+ } catch (Throwable $exception) {
+ return $this->handleException($exception, $request);
+ }
+ }
+
+ /**
+ * Handle an exception and generate an error response
+ *
+ * @param \Throwable $exception The exception to handle.
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @return \Psr\Http\Message\ResponseInterface A response
+ */
+ public function handleException(Throwable $exception, ServerRequestInterface $request): ResponseInterface
+ {
+ $errorHandler = $this->getErrorHandler();
+ $renderer = $errorHandler->getRenderer($exception, $request);
+
+ try {
+ $errorHandler->logException($exception, $request);
+ $response = $renderer->render();
+ } catch (Throwable $internalException) {
+ $errorHandler->logException($internalException, $request);
+ $response = $this->handleInternalError();
+ }
+
+ return $response;
+ }
+
+ /**
+ * Convert a redirect exception into a response.
+ *
+ * @param \Cake\Http\Exception\RedirectException $exception The exception to handle
+ * @return \Psr\Http\Message\ResponseInterface Response created from the redirect.
+ */
+ public function handleRedirect(RedirectException $exception): ResponseInterface
+ {
+ return new RedirectResponse(
+ $exception->getMessage(),
+ $exception->getCode(),
+ $exception->getHeaders()
+ );
+ }
+
+ /**
+ * Handle internal errors.
+ *
+ * @return \Psr\Http\Message\ResponseInterface A response
+ */
+ protected function handleInternalError(): ResponseInterface
+ {
+ $response = new Response(['body' => 'An Internal Server Error Occurred']);
+
+ return $response->withStatus(500);
+ }
+
+ /**
+ * Get a error handler instance
+ *
+ * @return \Cake\Error\ErrorHandler The error handler.
+ */
+ protected function getErrorHandler(): ErrorHandler
+ {
+ if ($this->errorHandler === null) {
+ /** @var class-string<\Cake\Error\ErrorHandler> $className */
+ $className = App::className('ErrorHandler', 'Error');
+ $this->errorHandler = new $className($this->getConfig());
+ }
+
+ return $this->errorHandler;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/Decorator/AbstractDecorator.php b/app/vendor/cakephp/cakephp/src/Event/Decorator/AbstractDecorator.php
new file mode 100644
index 000000000..5061d607e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/Decorator/AbstractDecorator.php
@@ -0,0 +1,73 @@
+_callable = $callable;
+ $this->_options = $options;
+ }
+
+ /**
+ * Invoke
+ *
+ * @link https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke
+ * @return mixed
+ */
+ public function __invoke()
+ {
+ return $this->_call(func_get_args());
+ }
+
+ /**
+ * Calls the decorated callable with the passed arguments.
+ *
+ * @param array $args Arguments for the callable.
+ * @return mixed
+ */
+ protected function _call(array $args)
+ {
+ $callable = $this->_callable;
+
+ return $callable(...$args);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/Decorator/ConditionDecorator.php b/app/vendor/cakephp/cakephp/src/Event/Decorator/ConditionDecorator.php
new file mode 100644
index 000000000..92c7b9f3b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/Decorator/ConditionDecorator.php
@@ -0,0 +1,75 @@
+canTrigger($args[0])) {
+ return;
+ }
+
+ return $this->_call($args);
+ }
+
+ /**
+ * Checks if the event is triggered for this listener.
+ *
+ * @param \Cake\Event\EventInterface $event Event object.
+ * @return bool
+ */
+ public function canTrigger(EventInterface $event): bool
+ {
+ $if = $this->_evaluateCondition('if', $event);
+ $unless = $this->_evaluateCondition('unless', $event);
+
+ return $if && !$unless;
+ }
+
+ /**
+ * Evaluates the filter conditions
+ *
+ * @param string $condition Condition type
+ * @param \Cake\Event\EventInterface $event Event object
+ * @return bool
+ */
+ protected function _evaluateCondition(string $condition, EventInterface $event): bool
+ {
+ if (!isset($this->_options[$condition])) {
+ return $condition !== 'unless';
+ }
+ if (!is_callable($this->_options[$condition])) {
+ throw new RuntimeException(self::class . ' the `' . $condition . '` condition is not a callable!');
+ }
+
+ return (bool)$this->_options[$condition]($event);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/Decorator/SubjectFilterDecorator.php b/app/vendor/cakephp/cakephp/src/Event/Decorator/SubjectFilterDecorator.php
new file mode 100644
index 000000000..b802ed1ab
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/Decorator/SubjectFilterDecorator.php
@@ -0,0 +1,70 @@
+canTrigger($args[0])) {
+ return false;
+ }
+
+ return $this->_call($args);
+ }
+
+ /**
+ * Checks if the event is triggered for this listener.
+ *
+ * @param \Cake\Event\EventInterface $event Event object.
+ * @return bool
+ */
+ public function canTrigger(EventInterface $event): bool
+ {
+ if (!isset($this->_options['allowedSubject'])) {
+ throw new RuntimeException(self::class . ' Missing subject filter options!');
+ }
+ if (is_string($this->_options['allowedSubject'])) {
+ $this->_options['allowedSubject'] = [$this->_options['allowedSubject']];
+ }
+
+ try {
+ $subject = $event->getSubject();
+ } catch (CakeException $e) {
+ return false;
+ }
+
+ return in_array(get_class($subject), $this->_options['allowedSubject'], true);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/Event.php b/app/vendor/cakephp/cakephp/src/Event/Event.php
new file mode 100644
index 000000000..75084ca31
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/Event.php
@@ -0,0 +1,196 @@
+ $userData]);
+ * $event = new Event('User.afterRegister', $userModel);
+ * ```
+ *
+ * @param string $name Name of the event
+ * @param object|null $subject the object that this event applies to
+ * (usually the object that is generating the event).
+ * @param array|\ArrayAccess|null $data any value you wish to be transported
+ * with this event to it can be read by listeners.
+ * @psalm-param TSubject|null $subject
+ */
+ public function __construct(string $name, $subject = null, $data = null)
+ {
+ $this->_name = $name;
+ $this->_subject = $subject;
+ $this->_data = (array)$data;
+ }
+
+ /**
+ * Returns the name of this event. This is usually used as the event identifier
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->_name;
+ }
+
+ /**
+ * Returns the subject of this event
+ *
+ * If the event has no subject an exception will be raised.
+ *
+ * @return object
+ * @throws \Cake\Core\Exception\CakeException
+ * @psalm-return TSubject
+ * @psalm-suppress LessSpecificImplementedReturnType
+ */
+ public function getSubject()
+ {
+ if ($this->_subject === null) {
+ throw new CakeException('No subject set for this event');
+ }
+
+ return $this->_subject;
+ }
+
+ /**
+ * Stops the event from being used anymore
+ *
+ * @return void
+ */
+ public function stopPropagation(): void
+ {
+ $this->_stopped = true;
+ }
+
+ /**
+ * Check if the event is stopped
+ *
+ * @return bool True if the event is stopped
+ */
+ public function isStopped(): bool
+ {
+ return $this->_stopped;
+ }
+
+ /**
+ * The result value of the event listeners
+ *
+ * @return mixed
+ */
+ public function getResult()
+ {
+ return $this->result;
+ }
+
+ /**
+ * Listeners can attach a result value to the event.
+ *
+ * @param mixed $value The value to set.
+ * @return $this
+ */
+ public function setResult($value = null)
+ {
+ $this->result = $value;
+
+ return $this;
+ }
+
+ /**
+ * Access the event data/payload.
+ *
+ * @param string|null $key The data payload element to return, or null to return all data.
+ * @return array|mixed|null The data payload if $key is null, or the data value for the given $key.
+ * If the $key does not exist a null value is returned.
+ */
+ public function getData(?string $key = null)
+ {
+ if ($key !== null) {
+ return $this->_data[$key] ?? null;
+ }
+
+ /** @psalm-suppress RedundantCastGivenDocblockType */
+ return (array)$this->_data;
+ }
+
+ /**
+ * Assigns a value to the data/payload of this event.
+ *
+ * @param array|string $key An array will replace all payload data, and a key will set just that array item.
+ * @param mixed $value The value to set.
+ * @return $this
+ */
+ public function setData($key, $value = null)
+ {
+ if (is_array($key)) {
+ $this->_data = $key;
+ } else {
+ $this->_data[$key] = $value;
+ }
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/EventDispatcherInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventDispatcherInterface.php
new file mode 100644
index 000000000..3bd812d09
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/EventDispatcherInterface.php
@@ -0,0 +1,61 @@
+_eventManager === null) {
+ $this->_eventManager = new EventManager();
+ }
+
+ return $this->_eventManager;
+ }
+
+ /**
+ * Returns the Cake\Event\EventManagerInterface instance for this object.
+ *
+ * You can use this instance to register any new listeners or callbacks to the
+ * object events, or create your own events and trigger them at will.
+ *
+ * @param \Cake\Event\EventManagerInterface $eventManager the eventManager to set
+ * @return $this
+ */
+ public function setEventManager(EventManagerInterface $eventManager)
+ {
+ $this->_eventManager = $eventManager;
+
+ return $this;
+ }
+
+ /**
+ * Wrapper for creating and dispatching events.
+ *
+ * Returns a dispatched event.
+ *
+ * @param string $name Name of the event.
+ * @param array|null $data Any value you wish to be transported with this event to
+ * it can be read by listeners.
+ * @param object|null $subject The object that this event applies to
+ * ($this by default).
+ * @return \Cake\Event\EventInterface
+ */
+ public function dispatchEvent(string $name, ?array $data = null, ?object $subject = null): EventInterface
+ {
+ if ($subject === null) {
+ $subject = $this;
+ }
+
+ /** @var \Cake\Event\EventInterface $event */
+ $event = new $this->_eventClass($name, $subject, $data);
+ $this->getEventManager()->dispatch($event);
+
+ return $event;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/EventInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventInterface.php
new file mode 100644
index 000000000..e67aceed1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/EventInterface.php
@@ -0,0 +1,86 @@
+_events = [];
+ }
+
+ /**
+ * Adds an event to the list when event listing is enabled.
+ *
+ * @param \Cake\Event\EventInterface $event An event to the list of dispatched events.
+ * @return void
+ */
+ public function add(EventInterface $event): void
+ {
+ $this->_events[] = $event;
+ }
+
+ /**
+ * Whether a offset exists
+ *
+ * @link https://secure.php.net/manual/en/arrayaccess.offsetexists.php
+ * @param mixed $offset An offset to check for.
+ * @return bool True on success or false on failure.
+ */
+ public function offsetExists($offset): bool
+ {
+ return isset($this->_events[$offset]);
+ }
+
+ /**
+ * Offset to retrieve
+ *
+ * @link https://secure.php.net/manual/en/arrayaccess.offsetget.php
+ * @param mixed $offset The offset to retrieve.
+ * @return mixed Can return all value types.
+ */
+ public function offsetGet($offset)
+ {
+ if ($this->offsetExists($offset)) {
+ return $this->_events[$offset];
+ }
+
+ return null;
+ }
+
+ /**
+ * Offset to set
+ *
+ * @link https://secure.php.net/manual/en/arrayaccess.offsetset.php
+ * @param mixed $offset The offset to assign the value to.
+ * @param mixed $value The value to set.
+ * @return void
+ */
+ public function offsetSet($offset, $value): void
+ {
+ $this->_events[$offset] = $value;
+ }
+
+ /**
+ * Offset to unset
+ *
+ * @link https://secure.php.net/manual/en/arrayaccess.offsetunset.php
+ * @param mixed $offset The offset to unset.
+ * @return void
+ */
+ public function offsetUnset($offset): void
+ {
+ unset($this->_events[$offset]);
+ }
+
+ /**
+ * Count elements of an object
+ *
+ * @link https://secure.php.net/manual/en/countable.count.php
+ * @return int The custom count as an integer.
+ */
+ public function count(): int
+ {
+ return count($this->_events);
+ }
+
+ /**
+ * Checks if an event is in the list.
+ *
+ * @param string $name Event name.
+ * @return bool
+ */
+ public function hasEvent(string $name): bool
+ {
+ foreach ($this->_events as $event) {
+ if ($event->getName() === $name) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/EventListenerInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventListenerInterface.php
new file mode 100644
index 000000000..484cddf99
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/EventListenerInterface.php
@@ -0,0 +1,46 @@
+ 'sendEmail',
+ * 'Article.afterBuy' => 'decrementInventory',
+ * 'User.onRegister' => ['callable' => 'logRegistration', 'priority' => 20, 'passParams' => true]
+ * ];
+ * }
+ * ```
+ *
+ * @return array Associative array or event key names pointing to the function
+ * that should be called in the object when the respective event is fired
+ */
+ public function implementedEvents(): array;
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/EventManager.php b/app/vendor/cakephp/cakephp/src/Event/EventManager.php
new file mode 100644
index 000000000..e65ab1378
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/EventManager.php
@@ -0,0 +1,486 @@
+_isGlobal = true;
+
+ return static::$_generalManager;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function on($eventKey, $options = [], ?callable $callable = null)
+ {
+ if ($eventKey instanceof EventListenerInterface) {
+ $this->_attachSubscriber($eventKey);
+
+ return $this;
+ }
+
+ $argCount = func_num_args();
+ if ($argCount === 2) {
+ $this->_listeners[$eventKey][static::$defaultPriority][] = [
+ 'callable' => $options,
+ ];
+
+ return $this;
+ }
+
+ $priority = $options['priority'] ?? static::$defaultPriority;
+ $this->_listeners[$eventKey][$priority][] = [
+ 'callable' => $callable,
+ ];
+
+ return $this;
+ }
+
+ /**
+ * Auxiliary function to attach all implemented callbacks of a Cake\Event\EventListenerInterface class instance
+ * as individual methods on this manager
+ *
+ * @param \Cake\Event\EventListenerInterface $subscriber Event listener.
+ * @return void
+ */
+ protected function _attachSubscriber(EventListenerInterface $subscriber): void
+ {
+ foreach ($subscriber->implementedEvents() as $eventKey => $function) {
+ $options = [];
+ $method = $function;
+ if (is_array($function) && isset($function['callable'])) {
+ [$method, $options] = $this->_extractCallable($function, $subscriber);
+ } elseif (is_array($function) && is_numeric(key($function))) {
+ foreach ($function as $f) {
+ [$method, $options] = $this->_extractCallable($f, $subscriber);
+ $this->on($eventKey, $options, $method);
+ }
+ continue;
+ }
+ if (is_string($method)) {
+ $method = [$subscriber, $function];
+ }
+ $this->on($eventKey, $options, $method);
+ }
+ }
+
+ /**
+ * Auxiliary function to extract and return a PHP callback type out of the callable definition
+ * from the return value of the `implementedEvents` method on a Cake\Event\EventListenerInterface
+ *
+ * @param array $function the array taken from a handler definition for an event
+ * @param \Cake\Event\EventListenerInterface $object The handler object
+ * @return array
+ */
+ protected function _extractCallable(array $function, EventListenerInterface $object): array
+ {
+ /** @var callable $method */
+ $method = $function['callable'];
+ $options = $function;
+ unset($options['callable']);
+ if (is_string($method)) {
+ /** @var callable $method */
+ $method = [$object, $method];
+ }
+
+ return [$method, $options];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function off($eventKey, $callable = null)
+ {
+ if ($eventKey instanceof EventListenerInterface) {
+ $this->_detachSubscriber($eventKey);
+
+ return $this;
+ }
+
+ if (!is_string($eventKey)) {
+ if (!is_callable($eventKey)) {
+ throw new CakeException(
+ 'First argument of EventManager::off() must be ' .
+ ' string or EventListenerInterface instance or callable.'
+ );
+ }
+
+ foreach (array_keys($this->_listeners) as $name) {
+ $this->off($name, $eventKey);
+ }
+
+ return $this;
+ }
+
+ if ($callable instanceof EventListenerInterface) {
+ $this->_detachSubscriber($callable, $eventKey);
+
+ return $this;
+ }
+
+ if ($callable === null) {
+ unset($this->_listeners[$eventKey]);
+
+ return $this;
+ }
+
+ if (empty($this->_listeners[$eventKey])) {
+ return $this;
+ }
+
+ foreach ($this->_listeners[$eventKey] as $priority => $callables) {
+ foreach ($callables as $k => $callback) {
+ if ($callback['callable'] === $callable) {
+ unset($this->_listeners[$eventKey][$priority][$k]);
+ break;
+ }
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Auxiliary function to help detach all listeners provided by an object implementing EventListenerInterface
+ *
+ * @param \Cake\Event\EventListenerInterface $subscriber the subscriber to be detached
+ * @param string|null $eventKey optional event key name to unsubscribe the listener from
+ * @return void
+ */
+ protected function _detachSubscriber(EventListenerInterface $subscriber, ?string $eventKey = null): void
+ {
+ $events = $subscriber->implementedEvents();
+ if (!empty($eventKey) && empty($events[$eventKey])) {
+ return;
+ }
+ if (!empty($eventKey)) {
+ $events = [$eventKey => $events[$eventKey]];
+ }
+ foreach ($events as $key => $function) {
+ if (is_array($function)) {
+ if (is_numeric(key($function))) {
+ foreach ($function as $handler) {
+ $handler = $handler['callable'] ?? $handler;
+ $this->off($key, [$subscriber, $handler]);
+ }
+ continue;
+ }
+ $function = $function['callable'];
+ }
+ $this->off($key, [$subscriber, $function]);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dispatch($event): EventInterface
+ {
+ if (is_string($event)) {
+ $event = new Event($event);
+ }
+
+ $listeners = $this->listeners($event->getName());
+
+ if ($this->_trackEvents) {
+ $this->addEventToList($event);
+ }
+
+ if (!$this->_isGlobal && static::instance()->isTrackingEvents()) {
+ static::instance()->addEventToList($event);
+ }
+
+ if (empty($listeners)) {
+ return $event;
+ }
+
+ foreach ($listeners as $listener) {
+ if ($event->isStopped()) {
+ break;
+ }
+ $result = $this->_callListener($listener['callable'], $event);
+ if ($result === false) {
+ $event->stopPropagation();
+ }
+ if ($result !== null) {
+ $event->setResult($result);
+ }
+ }
+
+ return $event;
+ }
+
+ /**
+ * Calls a listener.
+ *
+ * @param callable $listener The listener to trigger.
+ * @param \Cake\Event\EventInterface $event Event instance.
+ * @return mixed The result of the $listener function.
+ */
+ protected function _callListener(callable $listener, EventInterface $event)
+ {
+ $data = (array)$event->getData();
+
+ return $listener($event, ...array_values($data));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function listeners(string $eventKey): array
+ {
+ $localListeners = [];
+ if (!$this->_isGlobal) {
+ $localListeners = $this->prioritisedListeners($eventKey);
+ $localListeners = empty($localListeners) ? [] : $localListeners;
+ }
+ $globalListeners = static::instance()->prioritisedListeners($eventKey);
+ $globalListeners = empty($globalListeners) ? [] : $globalListeners;
+
+ $priorities = array_merge(array_keys($globalListeners), array_keys($localListeners));
+ $priorities = array_unique($priorities);
+ asort($priorities);
+
+ $result = [];
+ foreach ($priorities as $priority) {
+ if (isset($globalListeners[$priority])) {
+ $result = array_merge($result, $globalListeners[$priority]);
+ }
+ if (isset($localListeners[$priority])) {
+ $result = array_merge($result, $localListeners[$priority]);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the listeners for the specified event key indexed by priority
+ *
+ * @param string $eventKey Event key.
+ * @return array
+ */
+ public function prioritisedListeners(string $eventKey): array
+ {
+ if (empty($this->_listeners[$eventKey])) {
+ return [];
+ }
+
+ return $this->_listeners[$eventKey];
+ }
+
+ /**
+ * Returns the listeners matching a specified pattern
+ *
+ * @param string $eventKeyPattern Pattern to match.
+ * @return array
+ */
+ public function matchingListeners(string $eventKeyPattern): array
+ {
+ $matchPattern = '/' . preg_quote($eventKeyPattern, '/') . '/';
+ $matches = array_intersect_key(
+ $this->_listeners,
+ array_flip(
+ preg_grep($matchPattern, array_keys($this->_listeners), 0)
+ )
+ );
+
+ return $matches;
+ }
+
+ /**
+ * Returns the event list.
+ *
+ * @return \Cake\Event\EventList|null
+ */
+ public function getEventList(): ?EventList
+ {
+ return $this->_eventList;
+ }
+
+ /**
+ * Adds an event to the list if the event list object is present.
+ *
+ * @param \Cake\Event\EventInterface $event An event to add to the list.
+ * @return $this
+ */
+ public function addEventToList(EventInterface $event)
+ {
+ if ($this->_eventList) {
+ $this->_eventList->add($event);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Enables / disables event tracking at runtime.
+ *
+ * @param bool $enabled True or false to enable / disable it.
+ * @return $this
+ */
+ public function trackEvents(bool $enabled)
+ {
+ $this->_trackEvents = $enabled;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether this manager is set up to track events
+ *
+ * @return bool
+ */
+ public function isTrackingEvents(): bool
+ {
+ return $this->_trackEvents && $this->_eventList;
+ }
+
+ /**
+ * Enables the listing of dispatched events.
+ *
+ * @param \Cake\Event\EventList $eventList The event list object to use.
+ * @return $this
+ */
+ public function setEventList(EventList $eventList)
+ {
+ $this->_eventList = $eventList;
+ $this->_trackEvents = true;
+
+ return $this;
+ }
+
+ /**
+ * Disables the listing of dispatched events.
+ *
+ * @return $this
+ */
+ public function unsetEventList()
+ {
+ $this->_eventList = null;
+ $this->_trackEvents = false;
+
+ return $this;
+ }
+
+ /**
+ * Debug friendly object properties.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $properties = get_object_vars($this);
+ $properties['_generalManager'] = '(object) EventManager';
+ $properties['_listeners'] = [];
+ foreach ($this->_listeners as $key => $priorities) {
+ $listenerCount = 0;
+ foreach ($priorities as $listeners) {
+ $listenerCount += count($listeners);
+ }
+ $properties['_listeners'][$key] = $listenerCount . ' listener(s)';
+ }
+ if ($this->_eventList) {
+ $count = count($this->_eventList);
+ for ($i = 0; $i < $count; $i++) {
+ $event = $this->_eventList[$i];
+ try {
+ $subject = $event->getSubject();
+ $properties['_dispatchedEvents'][] = $event->getName() . ' with subject ' . get_class($subject);
+ } catch (CakeException $e) {
+ $properties['_dispatchedEvents'][] = $event->getName() . ' with no subject';
+ }
+ }
+ } else {
+ $properties['_dispatchedEvents'] = null;
+ }
+ unset($properties['_eventList']);
+
+ return $properties;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/EventManagerInterface.php b/app/vendor/cakephp/cakephp/src/Event/EventManagerInterface.php
new file mode 100644
index 000000000..80d90a0e1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/EventManagerInterface.php
@@ -0,0 +1,112 @@
+on($listener);
+ * ```
+ *
+ * Binding with no options:
+ *
+ * ```
+ * $eventManager->on('Model.beforeSave', $callable);
+ * ```
+ *
+ * Binding with options:
+ *
+ * ```
+ * $eventManager->on('Model.beforeSave', ['priority' => 90], $callable);
+ * ```
+ *
+ * @param string|\Cake\Event\EventListenerInterface $eventKey The event unique identifier name
+ * with which the callback will be associated. If $eventKey is an instance of
+ * Cake\Event\EventListenerInterface its events will be bound using the `implementedEvents` methods.
+ *
+ * @param array|callable $options Either an array of options or the callable you wish to
+ * bind to $eventKey. If an array of options, the `priority` key can be used to define the order.
+ * Priorities are treated as queues. Lower values are called before higher ones, and multiple attachments
+ * added to the same priority queue will be treated in the order of insertion.
+ *
+ * @param callable|null $callable The callable function you want invoked.
+ * @return $this
+ * @throws \InvalidArgumentException When event key is missing or callable is not an
+ * instance of Cake\Event\EventListenerInterface.
+ */
+ public function on($eventKey, $options = [], ?callable $callable = null);
+
+ /**
+ * Remove a listener from the active listeners.
+ *
+ * Remove a EventListenerInterface entirely:
+ *
+ * ```
+ * $manager->off($listener);
+ * ```
+ *
+ * Remove all listeners for a given event:
+ *
+ * ```
+ * $manager->off('My.event');
+ * ```
+ *
+ * Remove a specific listener:
+ *
+ * ```
+ * $manager->off('My.event', $callback);
+ * ```
+ *
+ * Remove a callback from all events:
+ *
+ * ```
+ * $manager->off($callback);
+ * ```
+ *
+ * @param string|\Cake\Event\EventListenerInterface|callable $eventKey The event unique identifier name
+ * with which the callback has been associated, or the $listener you want to remove.
+ * @param \Cake\Event\EventListenerInterface|callable|null $callable The callback you want to detach.
+ * @return $this
+ */
+ public function off($eventKey, $callable = null);
+
+ /**
+ * Dispatches a new event to all configured listeners
+ *
+ * @param string|\Cake\Event\EventInterface $event The event key name or instance of EventInterface.
+ * @return \Cake\Event\EventInterface
+ * @triggers $event
+ */
+ public function dispatch($event): EventInterface;
+
+ /**
+ * Returns a list of all listeners for an eventKey in the order they should be called
+ *
+ * @param string $eventKey Event key.
+ * @return array
+ */
+ public function listeners(string $eventKey): array;
+}
diff --git a/app/vendor/cakephp/cakephp/src/Event/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Event/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Event/README.md b/app/vendor/cakephp/cakephp/src/Event/README.md
new file mode 100644
index 000000000..2b511957a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/README.md
@@ -0,0 +1,51 @@
+[](https://packagist.org/packages/cakephp/event)
+[](LICENSE.txt)
+
+# CakePHP Event Library
+
+This library emulates several aspects of how events are triggered and managed in popular JavaScript
+libraries such as jQuery: An event object is dispatched to all listeners. The event object holds information
+about the event, and provides the ability to stop event propagation at any point.
+Listeners can register themselves or can delegate this task to other objects and have the chance to alter the
+state and the event itself for the rest of the callbacks.
+
+## Usage
+
+Listeners need to be registered into a manager and events can then be triggered so that listeners can be informed
+of the action.
+
+```php
+use Cake\Event\Event;
+use Cake\Event\EventDispatcherTrait;
+
+class Orders
+{
+
+ use EventDispatcherTrait;
+
+ public function placeOrder($order)
+ {
+ $this->doStuff();
+ $event = new Event('Orders.afterPlace', $this, [
+ 'order' => $order
+ ]);
+ $this->getEventManager()->dispatch($event);
+ }
+}
+
+$orders = new Orders();
+$orders->getEventManager()->on(function ($event) {
+ // Do something after the order was placed
+ ...
+}, 'Orders.afterPlace');
+
+$orders->placeOrder($order);
+```
+
+The above code allows you to easily notify the other parts of the application that an order has been created.
+You can then do tasks like send email notifications, update stock, log relevant statistics and other tasks
+in separate objects that focus on those concerns.
+
+## Documentation
+
+Please make sure you check the [official documentation](https://book.cakephp.org/4/en/core-libraries/events.html)
diff --git a/app/vendor/cakephp/cakephp/src/Event/composer.json b/app/vendor/cakephp/cakephp/src/Event/composer.json
new file mode 100644
index 000000000..d3a9bc50b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Event/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "cakephp/event",
+ "description": "CakePHP event dispatcher library that helps implementing the observer pattern",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "event",
+ "dispatcher",
+ "observer pattern"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/event/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/event"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/core": "^4.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Event\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/File.php b/app/vendor/cakephp/cakephp/src/Filesystem/File.php
new file mode 100644
index 000000000..b146b71de
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Filesystem/File.php
@@ -0,0 +1,663 @@
+Folder = new Folder($splInfo->getPath(), $create, $mode);
+ if (!is_dir($path)) {
+ $this->name = ltrim($splInfo->getFilename(), '/\\');
+ }
+ $this->pwd();
+ $create && !$this->exists() && $this->safe($path) && $this->create();
+ }
+
+ /**
+ * Closes the current file if it is opened
+ */
+ public function __destruct()
+ {
+ $this->close();
+ }
+
+ /**
+ * Creates the file.
+ *
+ * @return bool Success
+ */
+ public function create(): bool
+ {
+ $dir = $this->Folder->pwd();
+
+ if (is_dir($dir) && is_writable($dir) && !$this->exists() && touch($this->path)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Opens the current file with a given $mode
+ *
+ * @param string $mode A valid 'fopen' mode string (r|w|a ...)
+ * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't
+ * @return bool True on success, false on failure
+ */
+ public function open(string $mode = 'r', bool $force = false): bool
+ {
+ if (!$force && is_resource($this->handle)) {
+ return true;
+ }
+ if ($this->exists() === false && $this->create() === false) {
+ return false;
+ }
+
+ $this->handle = fopen($this->path, $mode);
+
+ return is_resource($this->handle);
+ }
+
+ /**
+ * Return the contents of this file as a string.
+ *
+ * @param string|false $bytes where to start
+ * @param string $mode A `fread` compatible mode.
+ * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't
+ * @return string|false String on success, false on failure
+ */
+ public function read($bytes = false, string $mode = 'rb', bool $force = false)
+ {
+ if ($bytes === false && $this->lock === null) {
+ return file_get_contents($this->path);
+ }
+ if ($this->open($mode, $force) === false) {
+ return false;
+ }
+ if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) {
+ return false;
+ }
+ if (is_int($bytes)) {
+ return fread($this->handle, $bytes);
+ }
+
+ $data = '';
+ while (!feof($this->handle)) {
+ $data .= fgets($this->handle, 4096);
+ }
+
+ if ($this->lock !== null) {
+ flock($this->handle, LOCK_UN);
+ }
+ if ($bytes === false) {
+ $this->close();
+ }
+
+ return trim($data);
+ }
+
+ /**
+ * Sets or gets the offset for the currently opened file.
+ *
+ * @param int|false $offset The $offset in bytes to seek. If set to false then the current offset is returned.
+ * @param int $seek PHP Constant SEEK_SET | SEEK_CUR | SEEK_END determining what the $offset is relative to
+ * @return int|bool True on success, false on failure (set mode), false on failure
+ * or integer offset on success (get mode).
+ */
+ public function offset($offset = false, int $seek = SEEK_SET)
+ {
+ if ($offset === false) {
+ if (is_resource($this->handle)) {
+ return ftell($this->handle);
+ }
+ } elseif ($this->open() === true) {
+ return fseek($this->handle, $offset, $seek) === 0;
+ }
+
+ return false;
+ }
+
+ /**
+ * Prepares an ASCII string for writing. Converts line endings to the
+ * correct terminator for the current platform. If Windows, "\r\n" will be used,
+ * all other platforms will use "\n"
+ *
+ * @param string $data Data to prepare for writing.
+ * @param bool $forceWindows If true forces Windows new line string.
+ * @return string The with converted line endings.
+ */
+ public static function prepare(string $data, bool $forceWindows = false): string
+ {
+ $lineBreak = "\n";
+ if (DIRECTORY_SEPARATOR === '\\' || $forceWindows === true) {
+ $lineBreak = "\r\n";
+ }
+
+ return strtr($data, ["\r\n" => $lineBreak, "\n" => $lineBreak, "\r" => $lineBreak]);
+ }
+
+ /**
+ * Write given data to this file.
+ *
+ * @param string $data Data to write to this File.
+ * @param string $mode Mode of writing. {@link https://secure.php.net/fwrite See fwrite()}.
+ * @param bool $force Force the file to open
+ * @return bool Success
+ */
+ public function write(string $data, string $mode = 'w', bool $force = false): bool
+ {
+ $success = false;
+ if ($this->open($mode, $force) === true) {
+ if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) {
+ return false;
+ }
+
+ if (fwrite($this->handle, $data) !== false) {
+ $success = true;
+ }
+ if ($this->lock !== null) {
+ flock($this->handle, LOCK_UN);
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Append given data string to this file.
+ *
+ * @param string $data Data to write
+ * @param bool $force Force the file to open
+ * @return bool Success
+ */
+ public function append(string $data, bool $force = false): bool
+ {
+ return $this->write($data, 'a', $force);
+ }
+
+ /**
+ * Closes the current file if it is opened.
+ *
+ * @return bool True if closing was successful or file was already closed, otherwise false
+ */
+ public function close(): bool
+ {
+ if (!is_resource($this->handle)) {
+ return true;
+ }
+
+ return fclose($this->handle);
+ }
+
+ /**
+ * Deletes the file.
+ *
+ * @return bool Success
+ */
+ public function delete(): bool
+ {
+ $this->close();
+ $this->handle = null;
+ if ($this->exists()) {
+ return unlink($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the file info as an array with the following keys:
+ *
+ * - dirname
+ * - basename
+ * - extension
+ * - filename
+ * - filesize
+ * - mime
+ *
+ * @return array File information.
+ */
+ public function info(): array
+ {
+ if (!$this->info) {
+ $this->info = pathinfo($this->path);
+ }
+ if (!isset($this->info['filename'])) {
+ $this->info['filename'] = $this->name();
+ }
+ if (!isset($this->info['filesize'])) {
+ $this->info['filesize'] = $this->size();
+ }
+ if (!isset($this->info['mime'])) {
+ $this->info['mime'] = $this->mime();
+ }
+
+ return $this->info;
+ }
+
+ /**
+ * Returns the file extension.
+ *
+ * @return string|false The file extension, false if extension cannot be extracted.
+ */
+ public function ext()
+ {
+ if (!$this->info) {
+ $this->info();
+ }
+ if (isset($this->info['extension'])) {
+ return $this->info['extension'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the file name without extension.
+ *
+ * @return string|false The file name without extension, false if name cannot be extracted.
+ */
+ public function name()
+ {
+ if (!$this->info) {
+ $this->info();
+ }
+ if (isset($this->info['extension'])) {
+ return static::_basename($this->name, '.' . $this->info['extension']);
+ }
+ if ($this->name) {
+ return $this->name;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the file basename. simulate the php basename() for multibyte (mb_basename).
+ *
+ * @param string $path Path to file
+ * @param string|null $ext The name of the extension
+ * @return string the file basename.
+ */
+ protected static function _basename(string $path, ?string $ext = null): string
+ {
+ // check for multibyte string and use basename() if not found
+ if (mb_strlen($path) === strlen($path)) {
+ return $ext === null ? basename($path) : basename($path, $ext);
+ }
+
+ $splInfo = new SplFileInfo($path);
+ $name = ltrim($splInfo->getFilename(), '/\\');
+
+ if ($ext === null || $ext === '') {
+ return $name;
+ }
+ $ext = preg_quote($ext);
+ $new = preg_replace("/({$ext})$/u", '', $name);
+
+ // basename of '/etc/.d' is '.d' not ''
+ return $new === '' ? $name : $new;
+ }
+
+ /**
+ * Makes file name safe for saving
+ *
+ * @param string|null $name The name of the file to make safe if different from $this->name
+ * @param string|null $ext The name of the extension to make safe if different from $this->ext
+ * @return string The extension of the file
+ */
+ public function safe(?string $name = null, ?string $ext = null): string
+ {
+ if (!$name) {
+ $name = (string)$this->name;
+ }
+ if (!$ext) {
+ $ext = (string)$this->ext();
+ }
+
+ return preg_replace("/(?:[^\w\.-]+)/", '_', static::_basename($name, $ext));
+ }
+
+ /**
+ * Get md5 Checksum of file with previous check of Filesize
+ *
+ * @param int|true $maxsize in MB or true to force
+ * @return string|false md5 Checksum {@link https://secure.php.net/md5_file See md5_file()},
+ * or false in case of an error.
+ */
+ public function md5($maxsize = 5)
+ {
+ if ($maxsize === true) {
+ return md5_file($this->path);
+ }
+
+ $size = $this->size();
+ if ($size && $size < $maxsize * 1024 * 1024) {
+ return md5_file($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the full path of the file.
+ *
+ * @return string|false Full path to the file, or false on failure
+ */
+ public function pwd()
+ {
+ if ($this->path === null) {
+ $dir = $this->Folder->pwd();
+ if ($dir && is_dir($dir)) {
+ $this->path = $this->Folder->slashTerm($dir) . $this->name;
+ }
+ }
+
+ return $this->path;
+ }
+
+ /**
+ * Returns true if the file exists.
+ *
+ * @return bool True if it exists, false otherwise
+ */
+ public function exists(): bool
+ {
+ $this->clearStatCache();
+
+ return $this->path && file_exists($this->path) && is_file($this->path);
+ }
+
+ /**
+ * Returns the "chmod" (permissions) of the file.
+ *
+ * @return string|false Permissions for the file, or false in case of an error
+ */
+ public function perms()
+ {
+ if ($this->exists()) {
+ return decoct(fileperms($this->path) & 0777);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the file size
+ *
+ * @return int|false Size of the file in bytes, or false in case of an error
+ */
+ public function size()
+ {
+ if ($this->exists()) {
+ return filesize($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the file is writable.
+ *
+ * @return bool True if it's writable, false otherwise
+ */
+ public function writable(): bool
+ {
+ return is_writable($this->path);
+ }
+
+ /**
+ * Returns true if the File is executable.
+ *
+ * @return bool True if it's executable, false otherwise
+ */
+ public function executable(): bool
+ {
+ return is_executable($this->path);
+ }
+
+ /**
+ * Returns true if the file is readable.
+ *
+ * @return bool True if file is readable, false otherwise
+ */
+ public function readable(): bool
+ {
+ return is_readable($this->path);
+ }
+
+ /**
+ * Returns the file's owner.
+ *
+ * @return int|false The file owner, or bool in case of an error
+ */
+ public function owner()
+ {
+ if ($this->exists()) {
+ return fileowner($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the file's group.
+ *
+ * @return int|false The file group, or false in case of an error
+ */
+ public function group()
+ {
+ if ($this->exists()) {
+ return filegroup($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns last access time.
+ *
+ * @return int|false Timestamp of last access time, or false in case of an error
+ */
+ public function lastAccess()
+ {
+ if ($this->exists()) {
+ return fileatime($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns last modified time.
+ *
+ * @return int|false Timestamp of last modification, or false in case of an error
+ */
+ public function lastChange()
+ {
+ if ($this->exists()) {
+ return filemtime($this->path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the current folder.
+ *
+ * @return \Cake\Filesystem\Folder Current folder
+ */
+ public function folder(): Folder
+ {
+ return $this->Folder;
+ }
+
+ /**
+ * Copy the File to $dest
+ *
+ * @param string $dest Absolute path to copy the file to.
+ * @param bool $overwrite Overwrite $dest if exists
+ * @return bool Success
+ */
+ public function copy(string $dest, bool $overwrite = true): bool
+ {
+ if (!$this->exists() || is_file($dest) && !$overwrite) {
+ return false;
+ }
+
+ return copy($this->path, $dest);
+ }
+
+ /**
+ * Gets the mime type of the file. Uses the finfo extension if
+ * it's available, otherwise falls back to mime_content_type().
+ *
+ * @return string|false The mimetype of the file, or false if reading fails.
+ */
+ public function mime()
+ {
+ if (!$this->exists()) {
+ return false;
+ }
+ if (class_exists('finfo')) {
+ $finfo = new finfo(FILEINFO_MIME);
+ $type = $finfo->file($this->pwd());
+ if (!$type) {
+ return false;
+ }
+ [$type] = explode(';', $type);
+
+ return $type;
+ }
+ if (function_exists('mime_content_type')) {
+ return mime_content_type($this->pwd());
+ }
+
+ return false;
+ }
+
+ /**
+ * Clear PHP's internal stat cache
+ *
+ * @param bool $all Clear all cache or not. Passing false will clear
+ * the stat cache for the current path only.
+ * @return void
+ */
+ public function clearStatCache($all = false): void
+ {
+ if ($all === false && $this->path) {
+ clearstatcache(true, $this->path);
+ }
+
+ clearstatcache();
+ }
+
+ /**
+ * Searches for a given text and replaces the text if found.
+ *
+ * @param string|array $search Text(s) to search for.
+ * @param string|array $replace Text(s) to replace with.
+ * @return bool Success
+ */
+ public function replaceText($search, $replace): bool
+ {
+ if (!$this->open('r+')) {
+ return false;
+ }
+
+ if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) {
+ return false;
+ }
+
+ $replaced = $this->write(str_replace($search, $replace, $this->read()), 'w', true);
+
+ if ($this->lock !== null) {
+ flock($this->handle, LOCK_UN);
+ }
+ $this->close();
+
+ return $replaced;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/Filesystem.php b/app/vendor/cakephp/cakephp/src/Filesystem/Filesystem.php
new file mode 100644
index 000000000..d1ecebc3b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Filesystem/Filesystem.php
@@ -0,0 +1,277 @@
+filterIterator($directory, $filter);
+ }
+
+ /**
+ * Find files/ directories recursively in given directory path.
+ *
+ * @param string $path Directory path.
+ * @param mixed $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, $filter = null, ?int $flags = null): Iterator
+ {
+ $flags = $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 ($current->getFilename()[0] === '.' && $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 mixed $filter Regex string or callback.
+ * @return \Iterator
+ */
+ protected function filterIterator(Iterator $iterator, $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 0755.
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException When directory creation fails.
+ */
+ public function mkdir(string $dir, int $mode = 0755): 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;
+ 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
+ $result = $result && @rmdir($path);
+
+ return $result;
+ }
+
+ /**
+ * 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);
+ }
+
+ $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 strpos($path, '://') !== false;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/Folder.php b/app/vendor/cakephp/cakephp/src/Filesystem/Folder.php
new file mode 100644
index 000000000..83da3d71c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Filesystem/Folder.php
@@ -0,0 +1,987 @@
+ 'getPathname',
+ self::SORT_TIME => 'getCTime',
+ ];
+
+ /**
+ * Holds messages from last method.
+ *
+ * @var array
+ */
+ protected $_messages = [];
+
+ /**
+ * Holds errors from last method.
+ *
+ * @var array
+ */
+ protected $_errors = [];
+
+ /**
+ * Holds array of complete directory paths.
+ *
+ * @var array
+ */
+ protected $_directories;
+
+ /**
+ * Holds array of complete file paths.
+ *
+ * @var array
+ */
+ protected $_files;
+
+ /**
+ * Constructor.
+ *
+ * @param string|null $path Path to folder
+ * @param bool $create Create folder if not found
+ * @param int|null $mode Mode (CHMOD) to apply to created folder, false to ignore
+ */
+ public function __construct(?string $path = null, bool $create = false, ?int $mode = null)
+ {
+ if (empty($path)) {
+ $path = TMP;
+ }
+ if ($mode) {
+ $this->mode = $mode;
+ }
+
+ if (!file_exists($path) && $create === true) {
+ $this->create($path, $this->mode);
+ }
+ if (!Folder::isAbsolute($path)) {
+ $path = realpath($path);
+ }
+ if (!empty($path)) {
+ $this->cd($path);
+ }
+ }
+
+ /**
+ * Return current path.
+ *
+ * @return string|null Current path
+ */
+ public function pwd(): ?string
+ {
+ return $this->path;
+ }
+
+ /**
+ * Change directory to $path.
+ *
+ * @param string $path Path to the directory to change to
+ * @return string|false The new path. Returns false on failure
+ */
+ public function cd(string $path)
+ {
+ $path = $this->realpath($path);
+ if ($path !== false && is_dir($path)) {
+ return $this->path = $path;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an array of the contents of the current directory.
+ * The returned array holds two arrays: One of directories and one of files.
+ *
+ * @param string|bool $sort Whether you want the results sorted, set this and the sort property
+ * to false to get unsorted results.
+ * @param array|bool $exceptions Either an array or boolean true will not grab dot files
+ * @param bool $fullPath True returns the full path
+ * @return array Contents of current directory as an array, an empty array on failure
+ */
+ public function read($sort = self::SORT_NAME, $exceptions = false, bool $fullPath = false): array
+ {
+ $dirs = $files = [];
+
+ if (!$this->pwd()) {
+ return [$dirs, $files];
+ }
+ if (is_array($exceptions)) {
+ $exceptions = array_flip($exceptions);
+ }
+ $skipHidden = isset($exceptions['.']) || $exceptions === true;
+
+ try {
+ $iterator = new DirectoryIterator($this->path);
+ } catch (Exception $e) {
+ return [$dirs, $files];
+ }
+
+ if (!is_bool($sort) && isset($this->_fsorts[$sort])) {
+ $methodName = $this->_fsorts[$sort];
+ } else {
+ $methodName = $this->_fsorts[self::SORT_NAME];
+ }
+
+ foreach ($iterator as $item) {
+ if ($item->isDot()) {
+ continue;
+ }
+ $name = $item->getFilename();
+ if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
+ continue;
+ }
+ if ($fullPath) {
+ $name = $item->getPathname();
+ }
+
+ if ($item->isDir()) {
+ $dirs[$item->{$methodName}()][] = $name;
+ } else {
+ $files[$item->{$methodName}()][] = $name;
+ }
+ }
+
+ if ($sort || $this->sort) {
+ ksort($dirs);
+ ksort($files);
+ }
+
+ if ($dirs) {
+ $dirs = array_merge(...array_values($dirs));
+ }
+
+ if ($files) {
+ $files = array_merge(...array_values($files));
+ }
+
+ return [$dirs, $files];
+ }
+
+ /**
+ * Returns an array of all matching files in current directory.
+ *
+ * @param string $regexpPattern Preg_match pattern (Defaults to: .*)
+ * @param string|bool $sort Whether results should be sorted.
+ * @return array Files that match given pattern
+ */
+ public function find(string $regexpPattern = '.*', $sort = false): array
+ {
+ [, $files] = $this->read($sort);
+
+ return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files));
+ }
+
+ /**
+ * Returns an array of all matching files in and below current directory.
+ *
+ * @param string $pattern Preg_match pattern (Defaults to: .*)
+ * @param string|bool $sort Whether results should be sorted.
+ * @return array Files matching $pattern
+ */
+ public function findRecursive(string $pattern = '.*', $sort = false): array
+ {
+ if (!$this->pwd()) {
+ return [];
+ }
+ $startsOn = $this->path;
+ $out = $this->_findRecursive($pattern, $sort);
+ $this->cd($startsOn);
+
+ return $out;
+ }
+
+ /**
+ * Private helper function for findRecursive.
+ *
+ * @param string $pattern Pattern to match against
+ * @param bool $sort Whether results should be sorted.
+ * @return array Files matching pattern
+ */
+ protected function _findRecursive(string $pattern, bool $sort = false): array
+ {
+ [$dirs, $files] = $this->read($sort);
+ $found = [];
+
+ foreach ($files as $file) {
+ if (preg_match('/^' . $pattern . '$/i', $file)) {
+ $found[] = Folder::addPathElement($this->path, $file);
+ }
+ }
+ $start = $this->path;
+
+ foreach ($dirs as $dir) {
+ $this->cd(Folder::addPathElement($start, $dir));
+ $found = array_merge($found, $this->findRecursive($pattern, $sort));
+ }
+
+ return $found;
+ }
+
+ /**
+ * Returns true if given $path is a Windows path.
+ *
+ * @param string $path Path to check
+ * @return bool true if windows path, false otherwise
+ */
+ public static function isWindowsPath(string $path): bool
+ {
+ return preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\';
+ }
+
+ /**
+ * Returns true if given $path is an absolute path.
+ *
+ * @param string $path Path to check
+ * @return bool true if path is absolute.
+ */
+ public static function isAbsolute(string $path): bool
+ {
+ if (empty($path)) {
+ return false;
+ }
+
+ return $path[0] === '/' ||
+ preg_match('/^[A-Z]:\\\\/i', $path) ||
+ substr($path, 0, 2) === '\\\\' ||
+ self::isRegisteredStreamWrapper($path);
+ }
+
+ /**
+ * Returns true if given $path is a registered stream wrapper.
+ *
+ * @param string $path Path to check
+ * @return bool True if path is registered stream wrapper.
+ */
+ public static function isRegisteredStreamWrapper(string $path): bool
+ {
+ return preg_match('/^[^:\/\/]+?(?=:\/\/)/', $path, $matches) &&
+ in_array($matches[0], stream_get_wrappers(), true);
+ }
+
+ /**
+ * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
+ *
+ * @param string $path Path to transform
+ * @return string Path with the correct set of slashes ("\\" or "/")
+ */
+ public static function normalizeFullPath(string $path): string
+ {
+ $to = Folder::correctSlashFor($path);
+ $from = ($to === '/' ? '\\' : '/');
+
+ return str_replace($from, $to, $path);
+ }
+
+ /**
+ * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
+ *
+ * @param string $path Path to check
+ * @return string Set of slashes ("\\" or "/")
+ */
+ public static function correctSlashFor(string $path): string
+ {
+ return Folder::isWindowsPath($path) ? '\\' : '/';
+ }
+
+ /**
+ * Returns $path with added terminating slash (corrected for Windows or other OS).
+ *
+ * @param string $path Path to check
+ * @return string Path with ending slash
+ */
+ public static function slashTerm(string $path): string
+ {
+ if (Folder::isSlashTerm($path)) {
+ return $path;
+ }
+
+ return $path . Folder::correctSlashFor($path);
+ }
+
+ /**
+ * Returns $path with $element added, with correct slash in-between.
+ *
+ * @param string $path Path
+ * @param string|array $element Element to add at end of path
+ * @return string Combined path
+ */
+ public static function addPathElement(string $path, $element): string
+ {
+ $element = (array)$element;
+ array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR));
+
+ return implode(DIRECTORY_SEPARATOR, $element);
+ }
+
+ /**
+ * Returns true if the Folder is in the given path.
+ *
+ * @param string $path The absolute path to check that the current `pwd()` resides within.
+ * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`.
+ * @return bool
+ * @throws \InvalidArgumentException When the given `$path` argument is not an absolute path.
+ */
+ public function inPath(string $path, bool $reverse = false): bool
+ {
+ if (!Folder::isAbsolute($path)) {
+ throw new InvalidArgumentException('The $path argument is expected to be an absolute path.');
+ }
+
+ $dir = Folder::slashTerm($path);
+ $current = Folder::slashTerm($this->pwd());
+
+ if (!$reverse) {
+ $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current);
+ } else {
+ $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir);
+ }
+
+ return (bool)$return;
+ }
+
+ /**
+ * Change the mode on a directory structure recursively. This includes changing the mode on files as well.
+ *
+ * @param string $path The path to chmod.
+ * @param int|null $mode Octal value, e.g. 0755.
+ * @param bool $recursive Chmod recursively, set to false to only change the current directory.
+ * @param string[] $exceptions Array of files, directories to skip.
+ * @return bool Success.
+ */
+ public function chmod(string $path, ?int $mode = null, bool $recursive = true, array $exceptions = []): bool
+ {
+ if (!$mode) {
+ $mode = $this->mode;
+ }
+
+ if ($recursive === false && is_dir($path)) {
+ // phpcs:disable
+ if (@chmod($path, intval($mode, 8))) {
+ // phpcs:enable
+ $this->_messages[] = sprintf('%s changed to %s', $path, $mode);
+
+ return true;
+ }
+
+ $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode);
+
+ return false;
+ }
+
+ if (is_dir($path)) {
+ $paths = $this->tree($path);
+
+ foreach ($paths as $type) {
+ foreach ($type as $fullpath) {
+ $check = explode(DIRECTORY_SEPARATOR, $fullpath);
+ $count = count($check);
+
+ if (in_array($check[$count - 1], $exceptions, true)) {
+ continue;
+ }
+
+ // phpcs:disable
+ if (@chmod($fullpath, intval($mode, 8))) {
+ // phpcs:enable
+ $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode);
+ } else {
+ $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode);
+ }
+ }
+ }
+
+ if (empty($this->_errors)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an array of subdirectories for the provided or current path.
+ *
+ * @param string|null $path The directory path to get subdirectories for.
+ * @param bool $fullPath Whether to return the full path or only the directory name.
+ * @return array Array of subdirectories for the provided or current path.
+ */
+ public function subdirectories(?string $path = null, bool $fullPath = true): array
+ {
+ if (!$path) {
+ $path = $this->path;
+ }
+ $subdirectories = [];
+
+ try {
+ $iterator = new DirectoryIterator($path);
+ } catch (Exception $e) {
+ return [];
+ }
+
+ foreach ($iterator as $item) {
+ if (!$item->isDir() || $item->isDot()) {
+ continue;
+ }
+ $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename();
+ }
+
+ return $subdirectories;
+ }
+
+ /**
+ * Returns an array of nested directories and files in each directory
+ *
+ * @param string|null $path the directory path to build the tree from
+ * @param array|bool $exceptions Either an array of files/folder to exclude
+ * or boolean true to not grab dot files/folders
+ * @param string|null $type either 'file' or 'dir'. Null returns both files and directories
+ * @return array Array of nested directories and files in each directory
+ */
+ public function tree(?string $path = null, $exceptions = false, ?string $type = null): array
+ {
+ if (!$path) {
+ $path = $this->path;
+ }
+ $files = [];
+ $directories = [$path];
+
+ if (is_array($exceptions)) {
+ $exceptions = array_flip($exceptions);
+ }
+ $skipHidden = false;
+ if ($exceptions === true) {
+ $skipHidden = true;
+ } elseif (isset($exceptions['.'])) {
+ $skipHidden = true;
+ unset($exceptions['.']);
+ }
+
+ try {
+ $directory = new RecursiveDirectoryIterator(
+ $path,
+ RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF
+ );
+ $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
+ } catch (Exception $e) {
+ unset($directory, $iterator);
+
+ if ($type === null) {
+ return [[], []];
+ }
+
+ return [];
+ }
+
+ /**
+ * @var string $itemPath
+ * @var \RecursiveDirectoryIterator $fsIterator
+ */
+ foreach ($iterator as $itemPath => $fsIterator) {
+ if ($skipHidden) {
+ $subPathName = $fsIterator->getSubPathname();
+ if ($subPathName[0] === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) {
+ unset($fsIterator);
+ continue;
+ }
+ }
+ /** @var \FilesystemIterator $item */
+ $item = $fsIterator->current();
+ if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) {
+ unset($fsIterator, $item);
+ continue;
+ }
+
+ if ($item->isFile()) {
+ $files[] = $itemPath;
+ } elseif ($item->isDir() && !$item->isDot()) {
+ $directories[] = $itemPath;
+ }
+
+ // inner iterators need to be unset too in order for locks on parents to be released
+ unset($fsIterator, $item);
+ }
+
+ // unsetting iterators helps releasing possible locks in certain environments,
+ // which could otherwise make `rmdir()` fail
+ unset($directory, $iterator);
+
+ if ($type === null) {
+ return [$directories, $files];
+ }
+ if ($type === 'dir') {
+ return $directories;
+ }
+
+ return $files;
+ }
+
+ /**
+ * Create a directory structure recursively.
+ *
+ * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn`
+ *
+ * @param string $pathname The directory structure to create. Either an absolute or relative
+ * path. If the path is relative and exists in the process' cwd it will not be created.
+ * Otherwise relative paths will be prefixed with the current pwd().
+ * @param int|null $mode octal value 0755
+ * @return bool Returns TRUE on success, FALSE on failure
+ */
+ public function create(string $pathname, ?int $mode = null): bool
+ {
+ if (is_dir($pathname) || empty($pathname)) {
+ return true;
+ }
+
+ if (!self::isAbsolute($pathname)) {
+ $pathname = self::addPathElement($this->pwd(), $pathname);
+ }
+
+ if (!$mode) {
+ $mode = $this->mode;
+ }
+
+ if (is_file($pathname)) {
+ $this->_errors[] = sprintf('%s is a file', $pathname);
+
+ return false;
+ }
+ $pathname = rtrim($pathname, DIRECTORY_SEPARATOR);
+ $nextPathname = substr($pathname, 0, strrpos($pathname, DIRECTORY_SEPARATOR));
+
+ if ($this->create($nextPathname, $mode)) {
+ if (!file_exists($pathname)) {
+ $old = umask(0);
+ if (mkdir($pathname, $mode, true)) {
+ umask($old);
+ $this->_messages[] = sprintf('%s created', $pathname);
+
+ return true;
+ }
+ umask($old);
+ $this->_errors[] = sprintf('%s NOT created', $pathname);
+
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the size in bytes of this Folder and its contents.
+ *
+ * @return int size in bytes of current folder
+ */
+ public function dirsize(): int
+ {
+ $size = 0;
+ $directory = Folder::slashTerm($this->path);
+ $stack = [$directory];
+ $count = count($stack);
+ for ($i = 0, $j = $count; $i < $j; $i++) {
+ if (is_file($stack[$i])) {
+ $size += filesize($stack[$i]);
+ } elseif (is_dir($stack[$i])) {
+ $dir = dir($stack[$i]);
+ if ($dir) {
+ while (($entry = $dir->read()) !== false) {
+ if ($entry === '.' || $entry === '..') {
+ continue;
+ }
+ $add = $stack[$i] . $entry;
+
+ if (is_dir($stack[$i] . $entry)) {
+ $add = Folder::slashTerm($add);
+ }
+ $stack[] = $add;
+ }
+ $dir->close();
+ }
+ }
+ $j = count($stack);
+ }
+
+ return $size;
+ }
+
+ /**
+ * Recursively Remove directories if the system allows.
+ *
+ * @param string|null $path Path of directory to delete
+ * @return bool Success
+ */
+ public function delete(?string $path = null): bool
+ {
+ if (!$path) {
+ $path = $this->pwd();
+ }
+ if (!$path) {
+ return false;
+ }
+ $path = Folder::slashTerm($path);
+ if (is_dir($path)) {
+ try {
+ $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF);
+ $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST);
+ } catch (Exception $e) {
+ unset($directory, $iterator);
+
+ return false;
+ }
+
+ foreach ($iterator as $item) {
+ $filePath = $item->getPathname();
+ if ($item->isFile() || $item->isLink()) {
+ // phpcs:disable
+ if (@unlink($filePath)) {
+ // phpcs:enable
+ $this->_messages[] = sprintf('%s removed', $filePath);
+ } else {
+ $this->_errors[] = sprintf('%s NOT removed', $filePath);
+ }
+ } elseif ($item->isDir() && !$item->isDot()) {
+ // phpcs:disable
+ if (@rmdir($filePath)) {
+ // phpcs:enable
+ $this->_messages[] = sprintf('%s removed', $filePath);
+ } else {
+ $this->_errors[] = sprintf('%s NOT removed', $filePath);
+
+ unset($directory, $iterator, $item);
+
+ return false;
+ }
+ }
+
+ // inner iterators need to be unset too in order for locks on parents to be released
+ unset($item);
+ }
+
+ // unsetting iterators helps releasing possible locks in certain environments,
+ // which could otherwise make `rmdir()` fail
+ unset($directory, $iterator);
+
+ $path = rtrim($path, DIRECTORY_SEPARATOR);
+ // phpcs:disable
+ if (@rmdir($path)) {
+ // phpcs:enable
+ $this->_messages[] = sprintf('%s removed', $path);
+ } else {
+ $this->_errors[] = sprintf('%s NOT removed', $path);
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Recursive directory copy.
+ *
+ * ### Options
+ *
+ * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
+ * - `mode` The mode to copy the files/directories with as integer, e.g. 0775.
+ * - `skip` Files/directories to skip.
+ * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP
+ * - `recursive` Whether to copy recursively or not (default: true - recursive)
+ *
+ * @param string $to The directory to copy to.
+ * @param array $options Array of options (see above).
+ * @return bool Success.
+ */
+ public function copy(string $to, array $options = []): bool
+ {
+ if (!$this->pwd()) {
+ return false;
+ }
+ $options += [
+ 'from' => $this->path,
+ 'mode' => $this->mode,
+ 'skip' => [],
+ 'scheme' => Folder::MERGE,
+ 'recursive' => true,
+ ];
+
+ $fromDir = $options['from'];
+ $toDir = $to;
+ $mode = $options['mode'];
+
+ if (!$this->cd($fromDir)) {
+ $this->_errors[] = sprintf('%s not found', $fromDir);
+
+ return false;
+ }
+
+ if (!is_dir($toDir)) {
+ $this->create($toDir, $mode);
+ }
+
+ if (!is_writable($toDir)) {
+ $this->_errors[] = sprintf('%s not writable', $toDir);
+
+ return false;
+ }
+
+ $exceptions = array_merge(['.', '..', '.svn'], $options['skip']);
+ // phpcs:disable
+ if ($handle = @opendir($fromDir)) {
+ // phpcs:enable
+ while (($item = readdir($handle)) !== false) {
+ $to = Folder::addPathElement($toDir, $item);
+ if (($options['scheme'] !== Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions, true)) {
+ $from = Folder::addPathElement($fromDir, $item);
+ if (is_file($from) && (!is_file($to) || $options['scheme'] !== Folder::SKIP)) {
+ if (copy($from, $to)) {
+ chmod($to, intval($mode, 8));
+ touch($to, filemtime($from));
+ $this->_messages[] = sprintf('%s copied to %s', $from, $to);
+ } else {
+ $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to);
+ }
+ }
+
+ if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) {
+ $this->delete($to);
+ }
+
+ if (is_dir($from) && $options['recursive'] === false) {
+ continue;
+ }
+
+ if (is_dir($from) && !file_exists($to)) {
+ $old = umask(0);
+ if (mkdir($to, $mode, true)) {
+ umask($old);
+ $old = umask(0);
+ chmod($to, $mode);
+ umask($old);
+ $this->_messages[] = sprintf('%s created', $to);
+ $options = ['from' => $from] + $options;
+ $this->copy($to, $options);
+ } else {
+ $this->_errors[] = sprintf('%s not created', $to);
+ }
+ } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) {
+ $options = ['from' => $from] + $options;
+ $this->copy($to, $options);
+ }
+ }
+ }
+ closedir($handle);
+ } else {
+ return false;
+ }
+
+ return empty($this->_errors);
+ }
+
+ /**
+ * Recursive directory move.
+ *
+ * ### Options
+ *
+ * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
+ * - `mode` The mode to copy the files/directories with as integer, e.g. 0775.
+ * - `skip` Files/directories to skip.
+ * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP
+ * - `recursive` Whether to copy recursively or not (default: true - recursive)
+ *
+ * @param string $to The directory to move to.
+ * @param array $options Array of options (see above).
+ * @return bool Success
+ */
+ public function move(string $to, array $options = []): bool
+ {
+ $options += ['from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true];
+
+ if ($this->copy($to, $options) && $this->delete($options['from'])) {
+ return (bool)$this->cd($to);
+ }
+
+ return false;
+ }
+
+ /**
+ * get messages from latest method
+ *
+ * @param bool $reset Reset message stack after reading
+ * @return array
+ */
+ public function messages(bool $reset = true): array
+ {
+ $messages = $this->_messages;
+ if ($reset) {
+ $this->_messages = [];
+ }
+
+ return $messages;
+ }
+
+ /**
+ * get error from latest method
+ *
+ * @param bool $reset Reset error stack after reading
+ * @return array
+ */
+ public function errors(bool $reset = true): array
+ {
+ $errors = $this->_errors;
+ if ($reset) {
+ $this->_errors = [];
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Get the real path (taking ".." and such into account)
+ *
+ * @param string $path Path to resolve
+ * @return string|false The resolved path
+ */
+ public function realpath($path)
+ {
+ if (strpos($path, '..') === false) {
+ if (!Folder::isAbsolute($path)) {
+ $path = Folder::addPathElement($this->path, $path);
+ }
+
+ return $path;
+ }
+ $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path));
+ $parts = explode(DIRECTORY_SEPARATOR, $path);
+ $newparts = [];
+ $newpath = '';
+ if ($path[0] === DIRECTORY_SEPARATOR) {
+ $newpath = DIRECTORY_SEPARATOR;
+ }
+
+ while (($part = array_shift($parts)) !== null) {
+ if ($part === '.' || $part === '') {
+ continue;
+ }
+ if ($part === '..') {
+ if (!empty($newparts)) {
+ array_pop($newparts);
+ continue;
+ }
+
+ return false;
+ }
+ $newparts[] = $part;
+ }
+ $newpath .= implode(DIRECTORY_SEPARATOR, $newparts);
+
+ return Folder::slashTerm($newpath);
+ }
+
+ /**
+ * Returns true if given $path ends in a slash (i.e. is slash-terminated).
+ *
+ * @param string $path Path to check
+ * @return bool true if path ends with slash, false otherwise
+ */
+ public static function isSlashTerm(string $path): bool
+ {
+ $lastChar = $path[strlen($path) - 1];
+
+ return $lastChar === '/' || $lastChar === '\\';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Filesystem/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Filesystem/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/README.md b/app/vendor/cakephp/cakephp/src/Filesystem/README.md
new file mode 100644
index 000000000..ce2fa5c15
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Filesystem/README.md
@@ -0,0 +1,37 @@
+[](https://packagist.org/packages/cakephp/filesystem)
+[](LICENSE.txt)
+
+# This package has been deprecated.
+
+## CakePHP Filesystem Library
+
+The Folder and File utilities are convenience classes to help you read from and write/append to files; list files within a folder and other common directory related tasks.
+
+## Basic Usage
+
+Create a folder instance and search for all the `.php` files within it:
+
+```php
+use Cake\Filesystem\Folder;
+
+$dir = new Folder('/path/to/folder');
+$files = $dir->find('.*\.php');
+```
+
+Now you can loop through the files and read from or write/append to the contents or simply delete the file:
+
+```php
+foreach ($files as $file) {
+ $file = new File($dir->pwd() . DIRECTORY_SEPARATOR . $file);
+ $contents = $file->read();
+ // $file->write('I am overwriting the contents of this file');
+ // $file->append('I am adding to the bottom of this file.');
+ // $file->delete(); // I am deleting this file
+ $file->close(); // Be sure to close the file when you're done
+}
+```
+
+## Documentation
+
+Please make sure you check the [official
+documentation](https://book.cakephp.org/4/en/core-libraries/file-folder.html)
diff --git a/app/vendor/cakephp/cakephp/src/Filesystem/composer.json b/app/vendor/cakephp/cakephp/src/Filesystem/composer.json
new file mode 100644
index 000000000..edaffd5b6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Filesystem/composer.json
@@ -0,0 +1,34 @@
+{
+ "name": "cakephp/filesystem",
+ "description": "CakePHP filesystem convenience classes to help you work with files and folders.",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "filesystem",
+ "files",
+ "folders"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/filesystem/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/filesystem"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/core": "^4.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Filesystem\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Form/Form.php b/app/vendor/cakephp/cakephp/src/Form/Form.php
new file mode 100644
index 000000000..41563e253
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Form/Form.php
@@ -0,0 +1,359 @@
+
+ */
+ protected $_schemaClass = Schema::class;
+
+ /**
+ * The schema used by this form.
+ *
+ * @var \Cake\Form\Schema|null
+ */
+ protected $_schema;
+
+ /**
+ * The errors if any
+ *
+ * @var array
+ */
+ protected $_errors = [];
+
+ /**
+ * Form's data.
+ *
+ * @var array
+ */
+ protected $_data = [];
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\Event\EventManager|null $eventManager The event manager.
+ * Defaults to a new instance.
+ */
+ public function __construct(?EventManager $eventManager = null)
+ {
+ if ($eventManager !== null) {
+ $this->setEventManager($eventManager);
+ }
+
+ $this->getEventManager()->on($this);
+
+ if (method_exists($this, '_buildValidator')) {
+ deprecationWarning(
+ static::class . ' implements `_buildValidator` which is no longer used. ' .
+ 'You should implement `buildValidator(Validator $validator, string $name): void` ' .
+ 'or `validationDefault(Validator $validator): Validator` instead.'
+ );
+ }
+ }
+
+ /**
+ * Get the Form callbacks this form is interested in.
+ *
+ * The conventional method map is:
+ *
+ * - Form.buildValidator => buildValidator
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ if (method_exists($this, 'buildValidator')) {
+ return [
+ self::BUILD_VALIDATOR_EVENT => 'buildValidator',
+ ];
+ }
+
+ return [];
+ }
+
+ /**
+ * Set the schema for this form.
+ *
+ * @since 4.1.0
+ * @param \Cake\Form\Schema $schema The schema to set
+ * @return $this
+ */
+ public function setSchema(Schema $schema)
+ {
+ $this->_schema = $schema;
+
+ return $this;
+ }
+
+ /**
+ * Get the schema for this form.
+ *
+ * This method will call `_buildSchema()` when the schema
+ * is first built. This hook method lets you configure the
+ * schema or load a pre-defined one.
+ *
+ * @since 4.1.0
+ * @return \Cake\Form\Schema the schema instance.
+ */
+ public function getSchema(): Schema
+ {
+ if ($this->_schema === null) {
+ $this->_schema = $this->_buildSchema(new $this->_schemaClass());
+ }
+
+ return $this->_schema;
+ }
+
+ /**
+ * Get/Set the schema for this form.
+ *
+ * This method will call `_buildSchema()` when the schema
+ * is first built. This hook method lets you configure the
+ * schema or load a pre-defined one.
+ *
+ * @deprecated 4.1.0 Use {@link setSchema()}/{@link getSchema()} instead.
+ * @param \Cake\Form\Schema|null $schema The schema to set, or null.
+ * @return \Cake\Form\Schema the schema instance.
+ */
+ public function schema(?Schema $schema = null): Schema
+ {
+ deprecationWarning('Form::schema() is deprecated. Use setSchema() and getSchema() instead.');
+ if ($schema !== null) {
+ $this->setSchema($schema);
+ }
+
+ return $this->getSchema();
+ }
+
+ /**
+ * A hook method intended to be implemented by subclasses.
+ *
+ * You can use this method to define the schema using
+ * the methods on Cake\Form\Schema, or loads a pre-defined
+ * schema from a concrete class.
+ *
+ * @param \Cake\Form\Schema $schema The schema to customize.
+ * @return \Cake\Form\Schema The schema to use.
+ */
+ protected function _buildSchema(Schema $schema): Schema
+ {
+ return $schema;
+ }
+
+ /**
+ * Used to check if $data passes this form's validation.
+ *
+ * @param array $data The data to check.
+ * @return bool Whether or not the data is valid.
+ */
+ public function validate(array $data): bool
+ {
+ $validator = $this->getValidator();
+ $this->_errors = $validator->validate($data);
+
+ return count($this->_errors) === 0;
+ }
+
+ /**
+ * Get the errors in the form
+ *
+ * Will return the errors from the last call
+ * to `validate()` or `execute()`.
+ *
+ * @return array Last set validation errors.
+ */
+ public function getErrors(): array
+ {
+ return $this->_errors;
+ }
+
+ /**
+ * Set the errors in the form.
+ *
+ * ```
+ * $errors = [
+ * 'field_name' => ['rule_name' => 'message']
+ * ];
+ *
+ * $form->setErrors($errors);
+ * ```
+ *
+ * @param array $errors Errors list.
+ * @return $this
+ */
+ public function setErrors(array $errors)
+ {
+ $this->_errors = $errors;
+
+ return $this;
+ }
+
+ /**
+ * Execute the form if it is valid.
+ *
+ * First validates the form, then calls the `_execute()` hook method.
+ * This hook method can be implemented in subclasses to perform
+ * the action of the form. This may be sending email, interacting
+ * with a remote API, or anything else you may need.
+ *
+ * @param array $data Form data.
+ * @return bool False on validation failure, otherwise returns the
+ * result of the `_execute()` method.
+ */
+ public function execute(array $data): bool
+ {
+ $this->_data = $data;
+
+ if (!$this->validate($data)) {
+ return false;
+ }
+
+ return $this->_execute($data);
+ }
+
+ /**
+ * Hook method to be implemented in subclasses.
+ *
+ * Used by `execute()` to execute the form's action.
+ *
+ * @param array $data Form data.
+ * @return bool
+ */
+ protected function _execute(array $data): bool
+ {
+ return true;
+ }
+
+ /**
+ * Get field data.
+ *
+ * @param string|null $field The field name or null to get data array with
+ * all fields.
+ * @return mixed
+ */
+ public function getData(?string $field = null)
+ {
+ if ($field === null) {
+ return $this->_data;
+ }
+
+ return Hash::get($this->_data, $field);
+ }
+
+ /**
+ * Saves a variable or an associative array of variables for use inside form data.
+ *
+ * @param string|array $name The key to write, can be a dot notation value.
+ * Alternatively can be an array containing key(s) and value(s).
+ * @param mixed $value Value to set for var
+ * @return $this
+ */
+ public function set($name, $value = null)
+ {
+ $write = $name;
+ if (!is_array($name)) {
+ $write = [$name => $value];
+ }
+
+ /** @psalm-suppress PossiblyInvalidIterator */
+ foreach ($write as $key => $val) {
+ $this->_data = Hash::insert($this->_data, $key, $val);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set form data.
+ *
+ * @param array $data Data array.
+ * @return $this
+ */
+ public function setData(array $data)
+ {
+ $this->_data = $data;
+
+ return $this;
+ }
+
+ /**
+ * Get the printable version of a Form instance.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $special = [
+ '_schema' => $this->getSchema()->__debugInfo(),
+ '_errors' => $this->getErrors(),
+ '_validator' => $this->getValidator()->__debugInfo(),
+ ];
+
+ return $special + get_object_vars($this);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Form/FormProtector.php b/app/vendor/cakephp/cakephp/src/Form/FormProtector.php
new file mode 100644
index 000000000..e0776ce88
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Form/FormProtector.php
@@ -0,0 +1,591 @@
+unlockedFields = $data['unlockedFields'];
+ }
+ }
+
+ /**
+ * Validate submitted form data.
+ *
+ * @param mixed $formData Form data.
+ * @param string $url URL form was POSTed to.
+ * @param string $sessionId Session id for hash generation.
+ * @return bool
+ */
+ public function validate($formData, string $url, string $sessionId): bool
+ {
+ $this->debugMessage = null;
+
+ $extractedToken = $this->extractToken($formData);
+ if (empty($extractedToken)) {
+ return false;
+ }
+
+ $hashParts = $this->extractHashParts($formData);
+ $generatedToken = $this->generateHash(
+ $hashParts['fields'],
+ $hashParts['unlockedFields'],
+ $url,
+ $sessionId
+ );
+
+ if (hash_equals($generatedToken, $extractedToken)) {
+ return true;
+ }
+
+ if (Configure::read('debug')) {
+ $debugMessage = $this->debugTokenNotMatching($formData, $hashParts + compact('url', 'sessionId'));
+ if ($debugMessage) {
+ $this->debugMessage = $debugMessage;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine which fields of a form should be used for hash.
+ *
+ * @param string|array $field Reference to field to be secured. Can be dot
+ * separated string to indicate nesting or array of fieldname parts.
+ * @param bool $lock Whether this field should be part of the validation
+ * or excluded as part of the unlockedFields. Default `true`.
+ * @param mixed $value Field value, if value should not be tampered with.
+ * @return $this
+ */
+ public function addField($field, bool $lock = true, $value = null)
+ {
+ if (is_string($field)) {
+ $field = $this->getFieldNameArray($field);
+ }
+
+ if (empty($field)) {
+ return $this;
+ }
+
+ foreach ($this->unlockedFields as $unlockField) {
+ $unlockParts = explode('.', $unlockField);
+ if (array_values(array_intersect($field, $unlockParts)) === $unlockParts) {
+ return $this;
+ }
+ }
+
+ $field = implode('.', $field);
+ $field = preg_replace('/(\.\d+)+$/', '', $field);
+
+ if ($lock) {
+ if (!in_array($field, $this->fields, true)) {
+ if ($value !== null) {
+ $this->fields[$field] = $value;
+
+ return $this;
+ }
+ if (isset($this->fields[$field])) {
+ unset($this->fields[$field]);
+ }
+ $this->fields[] = $field;
+ }
+ } else {
+ $this->unlockField($field);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Parses the field name to create a dot separated name value for use in
+ * field hash. If fieldname is of form Model[field] or Model.field an array of
+ * fieldname parts like ['Model', 'field'] is returned.
+ *
+ * @param string $name The form inputs name attribute.
+ * @return string[] Array of field name params like ['Model.field'] or
+ * ['Model', 'field'] for array fields or empty array if $name is empty.
+ */
+ protected function getFieldNameArray(string $name): array
+ {
+ if (empty($name) && $name !== '0') {
+ return [];
+ }
+
+ if (strpos($name, '[') === false) {
+ return Hash::filter(explode('.', $name));
+ }
+ $parts = explode('[', $name);
+ $parts = array_map(function ($el) {
+ return trim($el, ']');
+ }, $parts);
+
+ return Hash::filter($parts, 'strlen');
+ }
+
+ /**
+ * Add to the list of fields that are currently unlocked.
+ *
+ * Unlocked fields are not included in the field hash.
+ *
+ * @param string $name The dot separated name for the field.
+ * @return $this
+ */
+ public function unlockField($name)
+ {
+ if (!in_array($name, $this->unlockedFields, true)) {
+ $this->unlockedFields[] = $name;
+ }
+
+ $index = array_search($name, $this->fields, true);
+ if ($index !== false) {
+ unset($this->fields[$index]);
+ }
+ unset($this->fields[$name]);
+
+ return $this;
+ }
+
+ /**
+ * Get validation error message.
+ *
+ * @return string|null
+ */
+ public function getError(): ?string
+ {
+ return $this->debugMessage;
+ }
+
+ /**
+ * Extract token from data.
+ *
+ * @param mixed $formData Data to validate.
+ * @return string|null Fields token on success, null on failure.
+ */
+ protected function extractToken($formData): ?string
+ {
+ if (!is_array($formData)) {
+ $this->debugMessage = 'Request data is not an array.';
+
+ return null;
+ }
+
+ $message = '`%s` was not found in request data.';
+ if (!isset($formData['_Token'])) {
+ $this->debugMessage = sprintf($message, '_Token');
+
+ return null;
+ }
+ if (!isset($formData['_Token']['fields'])) {
+ $this->debugMessage = sprintf($message, '_Token.fields');
+
+ return null;
+ }
+ if (!is_string($formData['_Token']['fields'])) {
+ $this->debugMessage = '`_Token.fields` is invalid.';
+
+ return null;
+ }
+ if (!isset($formData['_Token']['unlocked'])) {
+ $this->debugMessage = sprintf($message, '_Token.unlocked');
+
+ return null;
+ }
+ if (Configure::read('debug') && !isset($formData['_Token']['debug'])) {
+ $this->debugMessage = sprintf($message, '_Token.debug');
+
+ return null;
+ }
+ if (!Configure::read('debug') && isset($formData['_Token']['debug'])) {
+ $this->debugMessage = 'Unexpected `_Token.debug` found in request data';
+
+ return null;
+ }
+
+ $token = urldecode($formData['_Token']['fields']);
+ if (strpos($token, ':')) {
+ [$token, ] = explode(':', $token, 2);
+ }
+
+ return $token;
+ }
+
+ /**
+ * Return hash parts for the token generation
+ *
+ * @param array $formData Form data.
+ * @return array
+ * @psalm-return array{fields: array, unlockedFields: array}
+ */
+ protected function extractHashParts(array $formData): array
+ {
+ $fields = $this->extractFields($formData);
+ $unlockedFields = $this->sortedUnlockedFields($formData);
+
+ return [
+ 'fields' => $fields,
+ 'unlockedFields' => $unlockedFields,
+ ];
+ }
+
+ /**
+ * Return the fields list for the hash calculation
+ *
+ * @param array $formData Data array
+ * @return array
+ */
+ protected function extractFields(array $formData): array
+ {
+ $locked = '';
+ $token = urldecode($formData['_Token']['fields']);
+ $unlocked = urldecode($formData['_Token']['unlocked']);
+
+ if (strpos($token, ':')) {
+ [, $locked] = explode(':', $token, 2);
+ }
+ unset($formData['_Token']);
+
+ $locked = $locked ? explode('|', $locked) : [];
+ $unlocked = $unlocked ? explode('|', $unlocked) : [];
+
+ $fields = Hash::flatten($formData);
+ $fieldList = array_keys($fields);
+ $multi = $lockedFields = [];
+ $isUnlocked = false;
+
+ foreach ($fieldList as $i => $key) {
+ if (is_string($key) && preg_match('/(\.\d+){1,10}$/', $key)) {
+ $multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key);
+ unset($fieldList[$i]);
+ } else {
+ $fieldList[$i] = (string)$key;
+ }
+ }
+ if (!empty($multi)) {
+ $fieldList += array_unique($multi);
+ }
+
+ $unlockedFields = array_unique(
+ array_merge(
+ $this->unlockedFields,
+ $unlocked
+ )
+ );
+
+ foreach ($fieldList as $i => $key) {
+ $isLocked = in_array($key, $locked, true);
+
+ if (!empty($unlockedFields)) {
+ foreach ($unlockedFields as $off) {
+ $off = explode('.', $off);
+ $field = array_values(array_intersect(explode('.', $key), $off));
+ $isUnlocked = ($field === $off);
+ if ($isUnlocked) {
+ break;
+ }
+ }
+ }
+
+ if ($isUnlocked || $isLocked) {
+ unset($fieldList[$i]);
+ if ($isLocked) {
+ $lockedFields[$key] = $fields[$key];
+ }
+ }
+ }
+ sort($fieldList, SORT_STRING);
+ ksort($lockedFields, SORT_STRING);
+ $fieldList += $lockedFields;
+
+ return $fieldList;
+ }
+
+ /**
+ * Get the sorted unlocked string
+ *
+ * @param array $formData Data array
+ * @return string[]
+ */
+ protected function sortedUnlockedFields(array $formData): array
+ {
+ $unlocked = urldecode($formData['_Token']['unlocked']);
+ if (empty($unlocked)) {
+ return [];
+ }
+
+ $unlocked = explode('|', $unlocked);
+ sort($unlocked, SORT_STRING);
+
+ return $unlocked;
+ }
+
+ /**
+ * Generate the token data.
+ *
+ * @param string $url Form URL.
+ * @param string $sessionId Session Id.
+ * @return array The token data.
+ * @psalm-return array{fields: string, unlocked: string, debug: string}
+ */
+ public function buildTokenData(string $url = '', string $sessionId = ''): array
+ {
+ $fields = $this->fields;
+ $unlockedFields = $this->unlockedFields;
+
+ $locked = [];
+ foreach ($fields as $key => $value) {
+ if (is_numeric($value)) {
+ $value = (string)$value;
+ }
+ if (!is_int($key)) {
+ $locked[$key] = $value;
+ unset($fields[$key]);
+ }
+ }
+
+ sort($unlockedFields, SORT_STRING);
+ sort($fields, SORT_STRING);
+ ksort($locked, SORT_STRING);
+ $fields += $locked;
+
+ $fields = $this->generateHash($fields, $unlockedFields, $url, $sessionId);
+ $locked = implode('|', array_keys($locked));
+
+ return [
+ 'fields' => urlencode($fields . ':' . $locked),
+ 'unlocked' => urlencode(implode('|', $unlockedFields)),
+ 'debug' => urlencode(json_encode([
+ $url,
+ $this->fields,
+ $this->unlockedFields,
+ ])),
+ ];
+ }
+
+ /**
+ * Generate validation hash.
+ *
+ * @param array $fields Fields list.
+ * @param array $unlockedFields Unlocked fields.
+ * @param string $url Form URL.
+ * @param string $sessionId Session Id.
+ * @return string
+ */
+ protected function generateHash(array $fields, array $unlockedFields, string $url, string $sessionId)
+ {
+ $hashParts = [
+ $url,
+ serialize($fields),
+ implode('|', $unlockedFields),
+ $sessionId,
+ ];
+
+ return hash_hmac('sha1', implode('', $hashParts), Security::getSalt());
+ }
+
+ /**
+ * Create a message for humans to understand why Security token is not matching
+ *
+ * @param array $formData Data.
+ * @param array $hashParts Elements used to generate the Token hash
+ * @return string Message explaining why the tokens are not matching
+ */
+ protected function debugTokenNotMatching(array $formData, array $hashParts): string
+ {
+ $messages = [];
+ if (!isset($formData['_Token']['debug'])) {
+ return 'Form protection debug token not found.';
+ }
+
+ $expectedParts = json_decode(urldecode($formData['_Token']['debug']), true);
+ if (!is_array($expectedParts) || count($expectedParts) !== 3) {
+ return 'Invalid form protection debug token.';
+ }
+ $expectedUrl = Hash::get($expectedParts, 0);
+ $url = Hash::get($hashParts, 'url');
+ if ($expectedUrl !== $url) {
+ $messages[] = sprintf('URL mismatch in POST data (expected `%s` but found `%s`)', $expectedUrl, $url);
+ }
+ $expectedFields = Hash::get($expectedParts, 1);
+ $dataFields = Hash::get($hashParts, 'fields') ?: [];
+ $fieldsMessages = $this->debugCheckFields(
+ (array)$dataFields,
+ $expectedFields,
+ 'Unexpected field `%s` in POST data',
+ 'Tampered field `%s` in POST data (expected value `%s` but found `%s`)',
+ 'Missing field `%s` in POST data'
+ );
+ $expectedUnlockedFields = Hash::get($expectedParts, 2);
+ $dataUnlockedFields = Hash::get($hashParts, 'unlockedFields') ?: [];
+ $unlockFieldsMessages = $this->debugCheckFields(
+ (array)$dataUnlockedFields,
+ $expectedUnlockedFields,
+ 'Unexpected unlocked field `%s` in POST data',
+ '',
+ 'Missing unlocked field: `%s`'
+ );
+
+ $messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages);
+
+ return implode(', ', $messages);
+ }
+
+ /**
+ * Iterates data array to check against expected
+ *
+ * @param array $dataFields Fields array, containing the POST data fields
+ * @param array $expectedFields Fields array, containing the expected fields we should have in POST
+ * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
+ * @param string $stringKeyMessage Message string if tampered found in
+ * data fields indexed by string (protected).
+ * @param string $missingMessage Message string if missing field
+ * @return string[] Messages
+ */
+ protected function debugCheckFields(
+ array $dataFields,
+ array $expectedFields = [],
+ string $intKeyMessage = '',
+ string $stringKeyMessage = '',
+ string $missingMessage = ''
+ ): array {
+ $messages = $this->matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage);
+ $expectedFieldsMessage = $this->debugExpectedFields($expectedFields, $missingMessage);
+ if ($expectedFieldsMessage !== null) {
+ $messages[] = $expectedFieldsMessage;
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields
+ * will be unset
+ *
+ * @param array $dataFields Fields array, containing the POST data fields
+ * @param array $expectedFields Fields array, containing the expected fields we should have in POST
+ * @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
+ * @param string $stringKeyMessage Message string if tampered found in
+ * data fields indexed by string (protected)
+ * @return string[] Error messages
+ */
+ protected function matchExistingFields(
+ array $dataFields,
+ array &$expectedFields,
+ string $intKeyMessage,
+ string $stringKeyMessage
+ ): array {
+ $messages = [];
+ foreach ($dataFields as $key => $value) {
+ if (is_int($key)) {
+ $foundKey = array_search($value, $expectedFields, true);
+ if ($foundKey === false) {
+ $messages[] = sprintf($intKeyMessage, $value);
+ } else {
+ unset($expectedFields[$foundKey]);
+ }
+ } else {
+ if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) {
+ $messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value);
+ }
+ unset($expectedFields[$key]);
+ }
+ }
+
+ return $messages;
+ }
+
+ /**
+ * Generate debug message for the expected fields
+ *
+ * @param array $expectedFields Expected fields
+ * @param string $missingMessage Message template
+ * @return string|null Error message about expected fields
+ */
+ protected function debugExpectedFields(array $expectedFields = [], string $missingMessage = ''): ?string
+ {
+ if (count($expectedFields) === 0) {
+ return null;
+ }
+
+ $expectedFieldNames = [];
+ foreach ($expectedFields as $key => $expectedField) {
+ if (is_int($key)) {
+ $expectedFieldNames[] = $expectedField;
+ } else {
+ $expectedFieldNames[] = $key;
+ }
+ }
+
+ return sprintf($missingMessage, implode(', ', $expectedFieldNames));
+ }
+
+ /**
+ * Return debug info
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'fields' => $this->fields,
+ 'unlockedFields' => $this->unlockedFields,
+ 'debugMessage' => $this->debugMessage,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Form/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Form/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Form/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Form/README.md b/app/vendor/cakephp/cakephp/src/Form/README.md
new file mode 100644
index 000000000..e7594cbc6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Form/README.md
@@ -0,0 +1,63 @@
+[](https://packagist.org/packages/cakephp/form)
+[](LICENSE.txt)
+
+# CakePHP Form Library
+
+Form abstraction used to create forms not tied to ORM backed models,
+or to other permanent datastores. Ideal for implementing forms on top of
+API services, or contact forms.
+
+## Usage
+
+
+```php
+use Cake\Form\Form;
+use Cake\Form\Schema;
+use Cake\Validation\Validator;
+
+class ContactForm extends Form
+{
+
+ protected function _buildSchema(Schema $schema)
+ {
+ return $schema->addField('name', 'string')
+ ->addField('email', ['type' => 'string'])
+ ->addField('body', ['type' => 'text']);
+ }
+
+ public function validationDefault(Validator $validator)
+ {
+ return $validator->add('name', 'length', [
+ 'rule' => ['minLength', 10],
+ 'message' => 'A name is required'
+ ])->add('email', 'format', [
+ 'rule' => 'email',
+ 'message' => 'A valid email address is required',
+ ]);
+ }
+
+ protected function _execute(array $data)
+ {
+ // Send an email.
+ return true;
+ }
+}
+```
+
+In the above example we see the 3 hook methods that forms provide:
+
+- `_buildSchema()` is used to define the schema data. You can define field type, length, and precision.
+- `validationDefault()` Gets a `Cake\Validation\Validator` instance that you can attach validators to.
+- `_execute()` lets you define the behavior you want to happen when `execute()` is called and the data is valid.
+
+You can always define additional public methods as you need as well.
+
+```php
+$contact = new ContactForm();
+$success = $contact->execute($data);
+$errors = $contact->getErrors();
+```
+
+## Documentation
+
+Please make sure you check the [official documentation](https://book.cakephp.org/4/en/core-libraries/form.html)
diff --git a/app/vendor/cakephp/cakephp/src/Form/Schema.php b/app/vendor/cakephp/cakephp/src/Form/Schema.php
new file mode 100644
index 000000000..9f9ae2fe8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Form/Schema.php
@@ -0,0 +1,143 @@
+ null,
+ 'length' => null,
+ 'precision' => null,
+ 'default' => null,
+ ];
+
+ /**
+ * Add multiple fields to the schema.
+ *
+ * @param array $fields The fields to add.
+ * @return $this
+ */
+ public function addFields(array $fields)
+ {
+ foreach ($fields as $name => $attrs) {
+ $this->addField($name, $attrs);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds a field to the schema.
+ *
+ * @param string $name The field name.
+ * @param string|array $attrs The attributes for the field, or the type
+ * as a string.
+ * @return $this
+ */
+ public function addField(string $name, $attrs)
+ {
+ if (is_string($attrs)) {
+ $attrs = ['type' => $attrs];
+ }
+ $attrs = array_intersect_key($attrs, $this->_fieldDefaults);
+ $this->_fields[$name] = $attrs + $this->_fieldDefaults;
+
+ return $this;
+ }
+
+ /**
+ * Removes a field to the schema.
+ *
+ * @param string $name The field to remove.
+ * @return $this
+ */
+ public function removeField(string $name)
+ {
+ unset($this->_fields[$name]);
+
+ return $this;
+ }
+
+ /**
+ * Get the list of fields in the schema.
+ *
+ * @return string[] The list of field names.
+ */
+ public function fields(): array
+ {
+ return array_keys($this->_fields);
+ }
+
+ /**
+ * Get the attributes for a given field.
+ *
+ * @param string $name The field name.
+ * @return array|null The attributes for a field, or null.
+ */
+ public function field(string $name): ?array
+ {
+ if (!isset($this->_fields[$name])) {
+ return null;
+ }
+
+ return $this->_fields[$name];
+ }
+
+ /**
+ * Get the type of the named field.
+ *
+ * @param string $name The name of the field.
+ * @return string|null Either the field type or null if the
+ * field does not exist.
+ */
+ public function fieldType(string $name): ?string
+ {
+ $field = $this->field($name);
+ if (!$field) {
+ return null;
+ }
+
+ return $field['type'];
+ }
+
+ /**
+ * Get the printable version of this object
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ '_fields' => $this->_fields,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Form/composer.json b/app/vendor/cakephp/cakephp/src/Form/composer.json
new file mode 100644
index 000000000..794281138
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Form/composer.json
@@ -0,0 +1,33 @@
+{
+ "name": "cakephp/form",
+ "description": "CakePHP Form library",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "form"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/form/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/form"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/event": "^4.0",
+ "cakephp/validation": "^4.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Form\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/BaseApplication.php b/app/vendor/cakephp/cakephp/src/Http/BaseApplication.php
new file mode 100644
index 000000000..f6c9867a0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/BaseApplication.php
@@ -0,0 +1,315 @@
+configDir = rtrim($configDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ $this->plugins = Plugin::getCollection();
+ $this->_eventManager = $eventManager ?: EventManager::instance();
+ $this->controllerFactory = $controllerFactory;
+ }
+
+ /**
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to set in your App Class
+ * @return \Cake\Http\MiddlewareQueue
+ */
+ abstract public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue;
+
+ /**
+ * @inheritDoc
+ */
+ public function pluginMiddleware(MiddlewareQueue $middleware): MiddlewareQueue
+ {
+ foreach ($this->plugins->with('middleware') as $plugin) {
+ $middleware = $plugin->middleware($middleware);
+ }
+
+ return $middleware;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addPlugin($name, array $config = [])
+ {
+ if (is_string($name)) {
+ $plugin = $this->plugins->create($name, $config);
+ } else {
+ $plugin = $name;
+ }
+ $this->plugins->add($plugin);
+
+ return $this;
+ }
+
+ /**
+ * Add an optional plugin
+ *
+ * If it isn't available, ignore it.
+ *
+ * @param string|\Cake\Core\PluginInterface $name The plugin name or plugin object.
+ * @param array $config The configuration data for the plugin if using a string for $name
+ * @return $this
+ */
+ public function addOptionalPlugin($name, array $config = [])
+ {
+ try {
+ $this->addPlugin($name, $config);
+ } catch (MissingPluginException $e) {
+ // Do not halt if the plugin is missing
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the plugin collection in use.
+ *
+ * @return \Cake\Core\PluginCollection
+ */
+ public function getPlugins(): PluginCollection
+ {
+ return $this->plugins;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function bootstrap(): void
+ {
+ require_once $this->configDir . 'bootstrap.php';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function pluginBootstrap(): void
+ {
+ foreach ($this->plugins->with('bootstrap') as $plugin) {
+ $plugin->bootstrap($this);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * By default this will load `config/routes.php` for ease of use and backwards compatibility.
+ *
+ * @param \Cake\Routing\RouteBuilder $routes A route builder to add routes into.
+ * @return void
+ */
+ public function routes(RouteBuilder $routes): void
+ {
+ // Only load routes if the router is empty
+ if (!Router::routes()) {
+ require $this->configDir . 'routes.php';
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function pluginRoutes(RouteBuilder $routes): RouteBuilder
+ {
+ foreach ($this->plugins->with('routes') as $plugin) {
+ $plugin->routes($routes);
+ }
+
+ return $routes;
+ }
+
+ /**
+ * Define the console commands for an application.
+ *
+ * By default all commands in CakePHP, plugins and the application will be
+ * loaded using conventions based names.
+ *
+ * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
+ * @return \Cake\Console\CommandCollection The updated collection.
+ */
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ return $commands->addMany($commands->autoDiscover());
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function pluginConsole(CommandCollection $commands): CommandCollection
+ {
+ foreach ($this->plugins->with('console') as $plugin) {
+ $commands = $plugin->console($commands);
+ }
+
+ return $commands;
+ }
+
+ /**
+ * Get the dependency injection container for the application.
+ *
+ * The first time the container is fetched it will be constructed
+ * and stored for future calls.
+ *
+ * @return \Cake\Core\ContainerInterface
+ */
+ public function getContainer(): ContainerInterface
+ {
+ if ($this->container === null) {
+ $this->container = $this->buildContainer();
+ }
+
+ return $this->container;
+ }
+
+ /**
+ * Build the service container
+ *
+ * Override this method if you need to use a custom container or
+ * want to change how the container is built.
+ *
+ * @return \Cake\Core\ContainerInterface
+ */
+ protected function buildContainer(): ContainerInterface
+ {
+ $container = new Container();
+ $this->services($container);
+ foreach ($this->plugins->with('services') as $plugin) {
+ $plugin->services($container);
+ }
+
+ $event = $this->dispatchEvent('Application.buildContainer', ['container' => $container]);
+ if ($event->getResult() instanceof ContainerInterface) {
+ return $event->getResult();
+ }
+
+ return $container;
+ }
+
+ /**
+ * Register application container services.
+ *
+ * @param \Cake\Core\ContainerInterface $container The Container to update.
+ * @return void
+ */
+ public function services(ContainerInterface $container): void
+ {
+ }
+
+ /**
+ * Invoke the application.
+ *
+ * - Convert the PSR response into CakePHP equivalents.
+ * - Create the controller that will handle this request.
+ * - Invoke the controller.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function handle(
+ ServerRequestInterface $request
+ ): ResponseInterface {
+ if ($this->controllerFactory === null) {
+ $this->controllerFactory = new ControllerFactory($this->getContainer());
+ }
+
+ if (Router::getRequest() !== $request) {
+ Router::setRequest($request);
+ }
+
+ $controller = $this->controllerFactory->create($request);
+
+ return $this->controllerFactory->invoke($controller);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/CallbackStream.php b/app/vendor/cakephp/cakephp/src/Http/CallbackStream.php
new file mode 100644
index 000000000..4f930e276
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/CallbackStream.php
@@ -0,0 +1,52 @@
+detach();
+ $result = '';
+ if (is_callable($callback)) {
+ $result = $callback();
+ }
+ if (!is_string($result)) {
+ return '';
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client.php b/app/vendor/cakephp/cakephp/src/Http/Client.php
new file mode 100644
index 000000000..74d0aee38
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client.php
@@ -0,0 +1,689 @@
+get('/users', [], ['type' => 'json']);
+ * ```
+ *
+ * The `type` option sets both the `Content-Type` and `Accept` header, to
+ * the same mime type. When using `type` you can use either a full mime
+ * type or an alias. If you need different types in the Accept and Content-Type
+ * headers you should set them manually and not use `type`
+ *
+ * ### Using authentication
+ *
+ * By using the `auth` key you can use authentication. The type sub option
+ * can be used to specify which authentication strategy you want to use.
+ * CakePHP comes with a few built-in strategies:
+ *
+ * - Basic
+ * - Digest
+ * - Oauth
+ *
+ * ### Using proxies
+ *
+ * By using the `proxy` key you can set authentication credentials for
+ * a proxy if you need to use one. The type sub option can be used to
+ * specify which authentication strategy you want to use.
+ * CakePHP comes with built-in support for basic authentication.
+ */
+class Client implements ClientInterface
+{
+ use InstanceConfigTrait;
+
+ /**
+ * Default configuration for the client.
+ *
+ * @var array
+ */
+ protected $_defaultConfig = [
+ 'adapter' => null,
+ 'host' => null,
+ 'port' => null,
+ 'scheme' => 'http',
+ 'basePath' => '',
+ 'timeout' => 30,
+ 'ssl_verify_peer' => true,
+ 'ssl_verify_peer_name' => true,
+ 'ssl_verify_depth' => 5,
+ 'ssl_verify_host' => true,
+ 'redirect' => false,
+ 'protocolVersion' => '1.1',
+ ];
+
+ /**
+ * List of cookies from responses made with this client.
+ *
+ * Cookies are indexed by the cookie's domain or
+ * request host name.
+ *
+ * @var \Cake\Http\Cookie\CookieCollection
+ */
+ protected $_cookies;
+
+ /**
+ * Adapter for sending requests.
+ *
+ * @var \Cake\Http\Client\AdapterInterface
+ */
+ protected $_adapter;
+
+ /**
+ * Create a new HTTP Client.
+ *
+ * ### Config options
+ *
+ * You can set the following options when creating a client:
+ *
+ * - host - The hostname to do requests on.
+ * - port - The port to use.
+ * - scheme - The default scheme/protocol to use. Defaults to http.
+ * - basePath - A path to append to the domain to use. (/api/v1/)
+ * - timeout - The timeout in seconds. Defaults to 30
+ * - ssl_verify_peer - Whether or not SSL certificates should be validated.
+ * Defaults to true.
+ * - ssl_verify_peer_name - Whether or not peer names should be validated.
+ * Defaults to true.
+ * - ssl_verify_depth - The maximum certificate chain depth to traverse.
+ * Defaults to 5.
+ * - ssl_verify_host - Verify that the certificate and hostname match.
+ * Defaults to true.
+ * - redirect - Number of redirects to follow. Defaults to false.
+ * - adapter - The adapter class name or instance. Defaults to
+ * \Cake\Http\Client\Adapter\Curl if `curl` extension is loaded else
+ * \Cake\Http\Client\Adapter\Stream.
+ * - protocolVersion - The HTTP protocol version to use. Defaults to 1.1
+ *
+ * @param array $config Config options for scoped clients.
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+
+ $adapter = $this->_config['adapter'];
+ if ($adapter === null) {
+ $adapter = Curl::class;
+
+ if (!extension_loaded('curl')) {
+ $adapter = Stream::class;
+ }
+ } else {
+ $this->setConfig('adapter', null);
+ }
+
+ if (is_string($adapter)) {
+ $adapter = new $adapter();
+ }
+
+ if (!$adapter instanceof AdapterInterface) {
+ throw new InvalidArgumentException('Adapter must be an instance of Cake\Http\Client\AdapterInterface');
+ }
+ $this->_adapter = $adapter;
+
+ if (!empty($this->_config['cookieJar'])) {
+ $this->_cookies = $this->_config['cookieJar'];
+ $this->setConfig('cookieJar', null);
+ } else {
+ $this->_cookies = new CookieCollection();
+ }
+ }
+
+ /**
+ * Client instance returned is scoped to the domain, port, and scheme parsed from the passed URL string. The passed
+ * string must have a scheme and a domain. Optionally, if a port is included in the string, the port will be scoped
+ * too. If a path is included in the URL, the client instance will build urls with it prepended.
+ * Other parts of the url string are ignored.
+ *
+ * @param string $url A string URL e.g. https://example.com
+ * @return static
+ * @throws \InvalidArgumentException
+ */
+ public static function createFromUrl(string $url)
+ {
+ $parts = parse_url($url);
+
+ if ($parts === false) {
+ throw new InvalidArgumentException('String ' . $url . ' did not parse');
+ }
+
+ $config = array_intersect_key($parts, ['scheme' => '', 'port' => '', 'host' => '', 'path' => '']);
+ $config = array_merge(['scheme' => '', 'host' => ''], $config);
+
+ if (empty($config['scheme']) || empty($config['host'])) {
+ throw new InvalidArgumentException('The URL was parsed but did not contain a scheme or host');
+ }
+
+ if (isset($config['path'])) {
+ $config['basePath'] = $config['path'];
+ unset($config['path']);
+ }
+
+ return new static($config);
+ }
+
+ /**
+ * Get the cookies stored in the Client.
+ *
+ * @return \Cake\Http\Cookie\CookieCollection
+ */
+ public function cookies(): CookieCollection
+ {
+ return $this->_cookies;
+ }
+
+ /**
+ * Adds a cookie to the Client collection.
+ *
+ * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function addCookie(CookieInterface $cookie)
+ {
+ if (!$cookie->getDomain() || !$cookie->getPath()) {
+ throw new InvalidArgumentException('Cookie must have a domain and a path set.');
+ }
+ $this->_cookies = $this->_cookies->add($cookie);
+
+ return $this;
+ }
+
+ /**
+ * Do a GET request.
+ *
+ * The $data argument supports a special `_content` key
+ * for providing a request body in a GET request. This is
+ * generally not used, but services like ElasticSearch use
+ * this feature.
+ *
+ * @param string $url The url or path you want to request.
+ * @param array|string $data The query data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function get(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $body = null;
+ if (is_array($data) && isset($data['_content'])) {
+ $body = $data['_content'];
+ unset($data['_content']);
+ }
+ $url = $this->buildUrl($url, $data, $options);
+
+ return $this->_doRequest(
+ Request::METHOD_GET,
+ $url,
+ $body,
+ $options
+ );
+ }
+
+ /**
+ * Do a POST request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param mixed $data The post data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function post(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, [], $options);
+
+ return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
+ }
+
+ /**
+ * Do a PUT request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param mixed $data The request data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function put(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, [], $options);
+
+ return $this->_doRequest(Request::METHOD_PUT, $url, $data, $options);
+ }
+
+ /**
+ * Do a PATCH request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param mixed $data The request data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function patch(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, [], $options);
+
+ return $this->_doRequest(Request::METHOD_PATCH, $url, $data, $options);
+ }
+
+ /**
+ * Do an OPTIONS request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param mixed $data The request data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function options(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, [], $options);
+
+ return $this->_doRequest(Request::METHOD_OPTIONS, $url, $data, $options);
+ }
+
+ /**
+ * Do a TRACE request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param mixed $data The request data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function trace(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, [], $options);
+
+ return $this->_doRequest(Request::METHOD_TRACE, $url, $data, $options);
+ }
+
+ /**
+ * Do a DELETE request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param mixed $data The request data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function delete(string $url, $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, [], $options);
+
+ return $this->_doRequest(Request::METHOD_DELETE, $url, $data, $options);
+ }
+
+ /**
+ * Do a HEAD request.
+ *
+ * @param string $url The url or path you want to request.
+ * @param array $data The query string data you want to send.
+ * @param array $options Additional options for the request.
+ * @return \Cake\Http\Client\Response
+ */
+ public function head(string $url, array $data = [], array $options = []): Response
+ {
+ $options = $this->_mergeOptions($options);
+ $url = $this->buildUrl($url, $data, $options);
+
+ return $this->_doRequest(Request::METHOD_HEAD, $url, '', $options);
+ }
+
+ /**
+ * Helper method for doing non-GET requests.
+ *
+ * @param string $method HTTP method.
+ * @param string $url URL to request.
+ * @param mixed $data The request body.
+ * @param array $options The options to use. Contains auth, proxy, etc.
+ * @return \Cake\Http\Client\Response
+ */
+ protected function _doRequest(string $method, string $url, $data, $options): Response
+ {
+ $request = $this->_createRequest(
+ $method,
+ $url,
+ $data,
+ $options
+ );
+
+ return $this->send($request, $options);
+ }
+
+ /**
+ * Does a recursive merge of the parameter with the scope config.
+ *
+ * @param array $options Options to merge.
+ * @return array Options merged with set config.
+ */
+ protected function _mergeOptions(array $options): array
+ {
+ return Hash::merge($this->_config, $options);
+ }
+
+ /**
+ * Sends a PSR-7 request and returns a PSR-7 response.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request Request instance.
+ * @return \Psr\Http\Message\ResponseInterface Response instance.
+ * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
+ */
+ public function sendRequest(RequestInterface $request): ResponseInterface
+ {
+ return $this->send($request, $this->_config);
+ }
+
+ /**
+ * Send a request.
+ *
+ * Used internally by other methods, but can also be used to send
+ * handcrafted Request objects.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request to send.
+ * @param array $options Additional options to use.
+ * @return \Cake\Http\Client\Response
+ */
+ public function send(RequestInterface $request, array $options = []): Response
+ {
+ $redirects = 0;
+ if (isset($options['redirect'])) {
+ $redirects = (int)$options['redirect'];
+ unset($options['redirect']);
+ }
+
+ do {
+ $response = $this->_sendRequest($request, $options);
+
+ $handleRedirect = $response->isRedirect() && $redirects-- > 0;
+ if ($handleRedirect) {
+ $url = $request->getUri();
+
+ $location = $response->getHeaderLine('Location');
+ $locationUrl = $this->buildUrl($location, [], [
+ 'host' => $url->getHost(),
+ 'port' => $url->getPort(),
+ 'scheme' => $url->getScheme(),
+ 'protocolRelative' => true,
+ ]);
+ $request = $request->withUri(new Uri($locationUrl));
+ $request = $this->_cookies->addToRequest($request, []);
+ }
+ } while ($handleRedirect);
+
+ return $response;
+ }
+
+ /**
+ * Send a request without redirection.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request to send.
+ * @param array $options Additional options to use.
+ * @return \Cake\Http\Client\Response
+ */
+ protected function _sendRequest(RequestInterface $request, array $options): Response
+ {
+ $responses = $this->_adapter->send($request, $options);
+ foreach ($responses as $response) {
+ $this->_cookies = $this->_cookies->addFromResponse($response, $request);
+ }
+
+ return array_pop($responses);
+ }
+
+ /**
+ * Generate a URL based on the scoped client options.
+ *
+ * @param string $url Either a full URL or just the path.
+ * @param string|array $query The query data for the URL.
+ * @param array $options The config options stored with Client::config()
+ * @return string A complete url with scheme, port, host, and path.
+ */
+ public function buildUrl(string $url, $query = [], array $options = []): string
+ {
+ if (empty($options) && empty($query)) {
+ return $url;
+ }
+ if ($query) {
+ $q = strpos($url, '?') === false ? '?' : '&';
+ $url .= $q;
+ $url .= is_string($query) ? $query : http_build_query($query);
+ }
+ $defaults = [
+ 'host' => null,
+ 'port' => null,
+ 'scheme' => 'http',
+ 'basePath' => '',
+ 'protocolRelative' => false,
+ ];
+ $options += $defaults;
+
+ if ($options['protocolRelative'] && preg_match('#^//#', $url)) {
+ $url = $options['scheme'] . ':' . $url;
+ }
+ if (preg_match('#^https?://#', $url)) {
+ return $url;
+ }
+
+ $defaultPorts = [
+ 'http' => 80,
+ 'https' => 443,
+ ];
+ $out = $options['scheme'] . '://' . $options['host'];
+ if ($options['port'] && (int)$options['port'] !== $defaultPorts[$options['scheme']]) {
+ $out .= ':' . $options['port'];
+ }
+ if (!empty($options['basePath'])) {
+ $out .= '/' . trim($options['basePath'], '/');
+ }
+ $out .= '/' . ltrim($url, '/');
+
+ return $out;
+ }
+
+ /**
+ * Creates a new request object based on the parameters.
+ *
+ * @param string $method HTTP method name.
+ * @param string $url The url including query string.
+ * @param mixed $data The request body.
+ * @param array $options The options to use. Contains auth, proxy, etc.
+ * @return \Cake\Http\Client\Request
+ */
+ protected function _createRequest(string $method, string $url, $data, $options): Request
+ {
+ $headers = (array)($options['headers'] ?? []);
+ if (isset($options['type'])) {
+ $headers = array_merge($headers, $this->_typeHeaders($options['type']));
+ }
+ if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) {
+ $headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
+ $request = new Request($url, $method, $headers, $data);
+ /** @var \Cake\Http\Client\Request $request */
+ $request = $request->withProtocolVersion($this->getConfig('protocolVersion'));
+ $cookies = $options['cookies'] ?? [];
+ /** @var \Cake\Http\Client\Request $request */
+ $request = $this->_cookies->addToRequest($request, $cookies);
+ if (isset($options['auth'])) {
+ $request = $this->_addAuthentication($request, $options);
+ }
+ if (isset($options['proxy'])) {
+ $request = $this->_addProxy($request, $options);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Returns headers for Accept/Content-Type based on a short type
+ * or full mime-type.
+ *
+ * @param string $type short type alias or full mimetype.
+ * @return string[] Headers to set on the request.
+ * @throws \Cake\Core\Exception\CakeException When an unknown type alias is used.
+ * @psalm-return array{Accept: string, Content-Type: string}
+ */
+ protected function _typeHeaders(string $type): array
+ {
+ if (strpos($type, '/') !== false) {
+ return [
+ 'Accept' => $type,
+ 'Content-Type' => $type,
+ ];
+ }
+ $typeMap = [
+ 'json' => 'application/json',
+ 'xml' => 'application/xml',
+ ];
+ if (!isset($typeMap[$type])) {
+ throw new CakeException("Unknown type alias '$type'.");
+ }
+
+ return [
+ 'Accept' => $typeMap[$type],
+ 'Content-Type' => $typeMap[$type],
+ ];
+ }
+
+ /**
+ * Add authentication headers to the request.
+ *
+ * Uses the authentication type to choose the correct strategy
+ * and use its methods to add headers.
+ *
+ * @param \Cake\Http\Client\Request $request The request to modify.
+ * @param array $options Array of options containing the 'auth' key.
+ * @return \Cake\Http\Client\Request The updated request object.
+ */
+ protected function _addAuthentication(Request $request, array $options): Request
+ {
+ $auth = $options['auth'];
+ /** @var \Cake\Http\Client\Auth\Basic $adapter */
+ $adapter = $this->_createAuth($auth, $options);
+
+ return $adapter->authentication($request, $options['auth']);
+ }
+
+ /**
+ * Add proxy authentication headers.
+ *
+ * Uses the authentication type to choose the correct strategy
+ * and use its methods to add headers.
+ *
+ * @param \Cake\Http\Client\Request $request The request to modify.
+ * @param array $options Array of options containing the 'proxy' key.
+ * @return \Cake\Http\Client\Request The updated request object.
+ */
+ protected function _addProxy(Request $request, array $options): Request
+ {
+ $auth = $options['proxy'];
+ /** @var \Cake\Http\Client\Auth\Basic $adapter */
+ $adapter = $this->_createAuth($auth, $options);
+
+ return $adapter->proxyAuthentication($request, $options['proxy']);
+ }
+
+ /**
+ * Create the authentication strategy.
+ *
+ * Use the configuration options to create the correct
+ * authentication strategy handler.
+ *
+ * @param array $auth The authentication options to use.
+ * @param array $options The overall request options to use.
+ * @return object Authentication strategy instance.
+ * @throws \Cake\Core\Exception\CakeException when an invalid strategy is chosen.
+ */
+ protected function _createAuth(array $auth, array $options)
+ {
+ if (empty($auth['type'])) {
+ $auth['type'] = 'basic';
+ }
+ $name = ucfirst($auth['type']);
+ $class = App::className($name, 'Http/Client/Auth');
+ if (!$class) {
+ throw new CakeException(
+ sprintf('Invalid authentication type %s', $name)
+ );
+ }
+
+ return new $class($this, $options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Curl.php b/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Curl.php
new file mode 100644
index 000000000..e4db3d04e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Curl.php
@@ -0,0 +1,218 @@
+buildOptions($request, $options);
+ curl_setopt_array($ch, $options);
+
+ /** @var string|false $body */
+ $body = $this->exec($ch);
+ if ($body === false) {
+ $errorCode = curl_errno($ch);
+ $error = curl_error($ch);
+ curl_close($ch);
+
+ $message = "cURL Error ({$errorCode}) {$error}";
+ $errorNumbers = [
+ CURLE_FAILED_INIT,
+ CURLE_URL_MALFORMAT,
+ CURLE_URL_MALFORMAT_USER,
+ ];
+ if (in_array($errorCode, $errorNumbers, true)) {
+ throw new RequestException($message, $request);
+ }
+ throw new NetworkException($message, $request);
+ }
+
+ $responses = $this->createResponse($ch, $body);
+ curl_close($ch);
+
+ return $responses;
+ }
+
+ /**
+ * Convert client options into curl options.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request.
+ * @param array $options The client options
+ * @return array
+ */
+ public function buildOptions(RequestInterface $request, array $options): array
+ {
+ $headers = [];
+ foreach ($request->getHeaders() as $key => $values) {
+ $headers[] = $key . ': ' . implode(', ', $values);
+ }
+
+ $out = [
+ CURLOPT_URL => (string)$request->getUri(),
+ CURLOPT_HTTP_VERSION => $this->getProtocolVersion($request),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HEADER => true,
+ CURLOPT_HTTPHEADER => $headers,
+ ];
+ switch ($request->getMethod()) {
+ case Request::METHOD_GET:
+ $out[CURLOPT_HTTPGET] = true;
+ break;
+
+ case Request::METHOD_POST:
+ $out[CURLOPT_POST] = true;
+ break;
+
+ case Request::METHOD_HEAD:
+ $out[CURLOPT_NOBODY] = true;
+ break;
+
+ default:
+ $out[CURLOPT_POST] = true;
+ $out[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
+ break;
+ }
+
+ $body = $request->getBody();
+ $body->rewind();
+ $out[CURLOPT_POSTFIELDS] = $body->getContents();
+ // GET requests with bodies require custom request to be used.
+ if (strlen($out[CURLOPT_POSTFIELDS]) && isset($out[CURLOPT_HTTPGET])) {
+ $out[CURLOPT_CUSTOMREQUEST] = 'get';
+ }
+ if ($out[CURLOPT_POSTFIELDS] === '') {
+ unset($out[CURLOPT_POSTFIELDS]);
+ }
+
+ if (empty($options['ssl_cafile'])) {
+ $options['ssl_cafile'] = CaBundle::getBundledCaBundlePath();
+ }
+ if (!empty($options['ssl_verify_host'])) {
+ // Value of 1 or true is deprecated. Only 2 or 0 should be used now.
+ $options['ssl_verify_host'] = 2;
+ }
+ $optionMap = [
+ 'timeout' => CURLOPT_TIMEOUT,
+ 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
+ 'ssl_verify_host' => CURLOPT_SSL_VERIFYHOST,
+ 'ssl_cafile' => CURLOPT_CAINFO,
+ 'ssl_local_cert' => CURLOPT_SSLCERT,
+ 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD,
+ ];
+ foreach ($optionMap as $option => $curlOpt) {
+ if (isset($options[$option])) {
+ $out[$curlOpt] = $options[$option];
+ }
+ }
+ if (isset($options['proxy']['proxy'])) {
+ $out[CURLOPT_PROXY] = $options['proxy']['proxy'];
+ }
+ if (isset($options['proxy']['username'])) {
+ $password = !empty($options['proxy']['password']) ? $options['proxy']['password'] : '';
+ $out[CURLOPT_PROXYUSERPWD] = $options['proxy']['username'] . ':' . $password;
+ }
+ if (isset($options['curl']) && is_array($options['curl'])) {
+ // Can't use array_merge() because keys will be re-ordered.
+ foreach ($options['curl'] as $key => $value) {
+ $out[$key] = $value;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Convert HTTP version number into curl value.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request to get a protocol version for.
+ * @return int
+ */
+ protected function getProtocolVersion(RequestInterface $request): int
+ {
+ switch ($request->getProtocolVersion()) {
+ case '1.0':
+ return CURL_HTTP_VERSION_1_0;
+ case '1.1':
+ return CURL_HTTP_VERSION_1_1;
+ case '2':
+ case '2.0':
+ if (defined('CURL_HTTP_VERSION_2TLS')) {
+ return CURL_HTTP_VERSION_2TLS;
+ }
+ if (defined('CURL_HTTP_VERSION_2_0')) {
+ return CURL_HTTP_VERSION_2_0;
+ }
+ throw new HttpException('libcurl 7.33 or greater required for HTTP/2 support');
+ }
+
+ return CURL_HTTP_VERSION_NONE;
+ }
+
+ /**
+ * Convert the raw curl response into an Http\Client\Response
+ *
+ * @param resource $handle Curl handle
+ * @param string $responseData string The response data from curl_exec
+ * @return \Cake\Http\Client\Response[]
+ */
+ protected function createResponse($handle, $responseData): array
+ {
+ $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
+ $headers = trim(substr($responseData, 0, $headerSize));
+ $body = substr($responseData, $headerSize);
+ $response = new Response(explode("\r\n", $headers), $body);
+
+ return [$response];
+ }
+
+ /**
+ * Execute the curl handle.
+ *
+ * @param resource $ch Curl Resource handle
+ * @return string|bool
+ */
+ protected function exec($ch)
+ {
+ return curl_exec($ch);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Stream.php b/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Stream.php
new file mode 100644
index 000000000..761b23a24
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Adapter/Stream.php
@@ -0,0 +1,344 @@
+_stream = null;
+ $this->_context = null;
+ $this->_contextOptions = [];
+ $this->_sslContextOptions = [];
+ $this->_connectionErrors = [];
+
+ $this->_buildContext($request, $options);
+
+ return $this->_send($request);
+ }
+
+ /**
+ * Create the response list based on the headers & content
+ *
+ * Creates one or many response objects based on the number
+ * of redirects that occurred.
+ *
+ * @param array $headers The list of headers from the request(s)
+ * @param string $content The response content.
+ * @return \Cake\Http\Client\Response[] The list of responses from the request(s)
+ */
+ public function createResponses(array $headers, string $content): array
+ {
+ $indexes = $responses = [];
+ foreach ($headers as $i => $header) {
+ if (strtoupper(substr($header, 0, 5)) === 'HTTP/') {
+ $indexes[] = $i;
+ }
+ }
+ $last = count($indexes) - 1;
+ foreach ($indexes as $i => $start) {
+ /** @psalm-suppress InvalidOperand */
+ $end = isset($indexes[$i + 1]) ? $indexes[$i + 1] - $start : null;
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $headerSlice = array_slice($headers, $start, $end);
+ $body = $i === $last ? $content : '';
+ $responses[] = $this->_buildResponse($headerSlice, $body);
+ }
+
+ return $responses;
+ }
+
+ /**
+ * Build the stream context out of the request object.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request to build context from.
+ * @param array $options Additional request options.
+ * @return void
+ */
+ protected function _buildContext(RequestInterface $request, array $options): void
+ {
+ $this->_buildContent($request, $options);
+ $this->_buildHeaders($request, $options);
+ $this->_buildOptions($request, $options);
+
+ $url = $request->getUri();
+ $scheme = parse_url((string)$url, PHP_URL_SCHEME);
+ if ($scheme === 'https') {
+ $this->_buildSslContext($request, $options);
+ }
+ $this->_context = stream_context_create([
+ 'http' => $this->_contextOptions,
+ 'ssl' => $this->_sslContextOptions,
+ ]);
+ }
+
+ /**
+ * Build the header context for the request.
+ *
+ * Creates cookies & headers.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request being sent.
+ * @param array $options Array of options to use.
+ * @return void
+ */
+ protected function _buildHeaders(RequestInterface $request, array $options): void
+ {
+ $headers = [];
+ foreach ($request->getHeaders() as $name => $values) {
+ $headers[] = sprintf('%s: %s', $name, implode(', ', $values));
+ }
+ $this->_contextOptions['header'] = implode("\r\n", $headers);
+ }
+
+ /**
+ * Builds the request content based on the request object.
+ *
+ * If the $request->body() is a string, it will be used as is.
+ * Array data will be processed with Cake\Http\Client\FormData
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request being sent.
+ * @param array $options Array of options to use.
+ * @return void
+ */
+ protected function _buildContent(RequestInterface $request, array $options): void
+ {
+ $body = $request->getBody();
+ if (empty($body)) {
+ $this->_contextOptions['content'] = '';
+
+ return;
+ }
+ $body->rewind();
+ $this->_contextOptions['content'] = $body->getContents();
+ }
+
+ /**
+ * Build miscellaneous options for the request.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request being sent.
+ * @param array $options Array of options to use.
+ * @return void
+ */
+ protected function _buildOptions(RequestInterface $request, array $options): void
+ {
+ $this->_contextOptions['method'] = $request->getMethod();
+ $this->_contextOptions['protocol_version'] = $request->getProtocolVersion();
+ $this->_contextOptions['ignore_errors'] = true;
+
+ if (isset($options['timeout'])) {
+ $this->_contextOptions['timeout'] = $options['timeout'];
+ }
+ // Redirects are handled in the client layer because of cookie handling issues.
+ $this->_contextOptions['max_redirects'] = 0;
+
+ if (isset($options['proxy']['proxy'])) {
+ $this->_contextOptions['request_fulluri'] = true;
+ $this->_contextOptions['proxy'] = $options['proxy']['proxy'];
+ }
+ }
+
+ /**
+ * Build SSL options for the request.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request being sent.
+ * @param array $options Array of options to use.
+ * @return void
+ */
+ protected function _buildSslContext(RequestInterface $request, array $options): void
+ {
+ $sslOptions = [
+ 'ssl_verify_peer',
+ 'ssl_verify_peer_name',
+ 'ssl_verify_depth',
+ 'ssl_allow_self_signed',
+ 'ssl_cafile',
+ 'ssl_local_cert',
+ 'ssl_local_pk',
+ 'ssl_passphrase',
+ ];
+ if (empty($options['ssl_cafile'])) {
+ $options['ssl_cafile'] = CaBundle::getBundledCaBundlePath();
+ }
+ if (!empty($options['ssl_verify_host'])) {
+ $url = $request->getUri();
+ $host = parse_url((string)$url, PHP_URL_HOST);
+ $this->_sslContextOptions['peer_name'] = $host;
+ }
+ foreach ($sslOptions as $key) {
+ if (isset($options[$key])) {
+ $name = substr($key, 4);
+ $this->_sslContextOptions[$name] = $options[$key];
+ }
+ }
+ }
+
+ /**
+ * Open the stream and send the request.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request object.
+ * @return array Array of populated Response objects
+ * @throws \Psr\Http\Client\NetworkExceptionInterface
+ */
+ protected function _send(RequestInterface $request): array
+ {
+ $deadline = false;
+ if (isset($this->_contextOptions['timeout']) && $this->_contextOptions['timeout'] > 0) {
+ $deadline = time() + $this->_contextOptions['timeout'];
+ }
+
+ $url = $request->getUri();
+ $this->_open((string)$url, $request);
+ $content = '';
+ $timedOut = false;
+
+ /** @psalm-suppress PossiblyNullArgument */
+ while (!feof($this->_stream)) {
+ if ($deadline !== false) {
+ stream_set_timeout($this->_stream, max($deadline - time(), 1));
+ }
+
+ $content .= fread($this->_stream, 8192);
+
+ $meta = stream_get_meta_data($this->_stream);
+ if ($meta['timed_out'] || ($deadline !== false && time() > $deadline)) {
+ $timedOut = true;
+ break;
+ }
+ }
+ /** @psalm-suppress PossiblyNullArgument */
+ $meta = stream_get_meta_data($this->_stream);
+ /** @psalm-suppress InvalidPropertyAssignmentValue */
+ fclose($this->_stream);
+
+ if ($timedOut) {
+ throw new NetworkException('Connection timed out ' . $url, $request);
+ }
+
+ $headers = $meta['wrapper_data'];
+ if (isset($headers['headers']) && is_array($headers['headers'])) {
+ $headers = $headers['headers'];
+ }
+
+ return $this->createResponses($headers, $content);
+ }
+
+ /**
+ * Build a response object
+ *
+ * @param array $headers Unparsed headers.
+ * @param string $body The response body.
+ * @return \Cake\Http\Client\Response
+ */
+ protected function _buildResponse(array $headers, string $body): Response
+ {
+ return new Response($headers, $body);
+ }
+
+ /**
+ * Open the socket and handle any connection errors.
+ *
+ * @param string $url The url to connect to.
+ * @param \Psr\Http\Message\RequestInterface $request The request object.
+ * @return void
+ * @throws \Psr\Http\Client\RequestExceptionInterface
+ */
+ protected function _open(string $url, RequestInterface $request): void
+ {
+ if (!(bool)ini_get('allow_url_fopen')) {
+ throw new ClientException('The PHP directive `allow_url_fopen` must be enabled.');
+ }
+
+ set_error_handler(function ($code, $message): bool {
+ $this->_connectionErrors[] = $message;
+
+ return true;
+ });
+ try {
+ /** @psalm-suppress PossiblyNullArgument */
+ $this->_stream = fopen($url, 'rb', false, $this->_context);
+ } finally {
+ restore_error_handler();
+ }
+
+ if (!$this->_stream || !empty($this->_connectionErrors)) {
+ throw new RequestException(implode("\n", $this->_connectionErrors), $request);
+ }
+ }
+
+ /**
+ * Get the context options
+ *
+ * Useful for debugging and testing context creation.
+ *
+ * @return array
+ */
+ public function contextOptions(): array
+ {
+ return array_merge($this->_contextOptions, $this->_sslContextOptions);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/AdapterInterface.php b/app/vendor/cakephp/cakephp/src/Http/Client/AdapterInterface.php
new file mode 100644
index 000000000..b64c81dab
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/AdapterInterface.php
@@ -0,0 +1,33 @@
+_generateHeader($credentials['username'], $credentials['password']);
+ /** @var \Cake\Http\Client\Request $request */
+ $request = $request->withHeader('Authorization', $value);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Proxy Authentication
+ *
+ * @param \Cake\Http\Client\Request $request Request instance.
+ * @param array $credentials Credentials.
+ * @return \Cake\Http\Client\Request The updated request.
+ * @see https://www.ietf.org/rfc/rfc2617.txt
+ */
+ public function proxyAuthentication(Request $request, array $credentials): Request
+ {
+ if (isset($credentials['username'], $credentials['password'])) {
+ $value = $this->_generateHeader($credentials['username'], $credentials['password']);
+ /** @var \Cake\Http\Client\Request $request */
+ $request = $request->withHeader('Proxy-Authorization', $value);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Generate basic [proxy] authentication header
+ *
+ * @param string $user Username.
+ * @param string $pass Password.
+ * @return string
+ */
+ protected function _generateHeader(string $user, string $pass): string
+ {
+ return 'Basic ' . base64_encode($user . ':' . $pass);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Digest.php b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Digest.php
new file mode 100644
index 000000000..ef313371d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Digest.php
@@ -0,0 +1,148 @@
+_client = $client;
+ }
+
+ /**
+ * Add Authorization header to the request.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $credentials Authentication credentials.
+ * @return \Cake\Http\Client\Request The updated request.
+ * @see https://www.ietf.org/rfc/rfc2617.txt
+ */
+ public function authentication(Request $request, array $credentials): Request
+ {
+ if (!isset($credentials['username'], $credentials['password'])) {
+ return $request;
+ }
+ if (!isset($credentials['realm'])) {
+ $credentials = $this->_getServerInfo($request, $credentials);
+ }
+ if (!isset($credentials['realm'])) {
+ return $request;
+ }
+ $value = $this->_generateHeader($request, $credentials);
+
+ return $request->withHeader('Authorization', $value);
+ }
+
+ /**
+ * Retrieve information about the authentication
+ *
+ * Will get the realm and other tokens by performing
+ * another request without authentication to get authentication
+ * challenge.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $credentials Authentication credentials.
+ * @return array modified credentials.
+ */
+ protected function _getServerInfo(Request $request, array $credentials): array
+ {
+ $response = $this->_client->get(
+ (string)$request->getUri(),
+ [],
+ ['auth' => ['type' => null]]
+ );
+
+ if (!$response->getHeader('WWW-Authenticate')) {
+ return [];
+ }
+ preg_match_all(
+ '@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@',
+ $response->getHeaderLine('WWW-Authenticate'),
+ $matches,
+ PREG_SET_ORDER
+ );
+ foreach ($matches as $match) {
+ $credentials[$match[1]] = $match[2];
+ }
+ if (!empty($credentials['qop']) && empty($credentials['nc'])) {
+ $credentials['nc'] = 1;
+ }
+
+ return $credentials;
+ }
+
+ /**
+ * Generate the header Authorization
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $credentials Authentication credentials.
+ * @return string
+ */
+ protected function _generateHeader(Request $request, array $credentials): string
+ {
+ $path = $request->getUri()->getPath();
+ $a1 = md5($credentials['username'] . ':' . $credentials['realm'] . ':' . $credentials['password']);
+ $a2 = md5($request->getMethod() . ':' . $path);
+ $nc = '';
+
+ if (empty($credentials['qop'])) {
+ $response = md5($a1 . ':' . $credentials['nonce'] . ':' . $a2);
+ } else {
+ $credentials['cnonce'] = uniqid();
+ $nc = sprintf('%08x', $credentials['nc']++);
+ $response = md5(
+ $a1 . ':' . $credentials['nonce'] . ':' . $nc . ':' . $credentials['cnonce'] . ':auth:' . $a2
+ );
+ }
+
+ $authHeader = 'Digest ';
+ $authHeader .= 'username="' . str_replace(['\\', '"'], ['\\\\', '\\"'], $credentials['username']) . '", ';
+ $authHeader .= 'realm="' . $credentials['realm'] . '", ';
+ $authHeader .= 'nonce="' . $credentials['nonce'] . '", ';
+ $authHeader .= 'uri="' . $path . '", ';
+ $authHeader .= 'response="' . $response . '"';
+ if (!empty($credentials['opaque'])) {
+ $authHeader .= ', opaque="' . $credentials['opaque'] . '"';
+ }
+ if (!empty($credentials['qop'])) {
+ $authHeader .= ', qop="auth", nc=' . $nc . ', cnonce="' . $credentials['cnonce'] . '"';
+ }
+
+ return $authHeader;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php
new file mode 100644
index 000000000..f066ee025
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php
@@ -0,0 +1,370 @@
+_hmacSha1($request, $credentials);
+ break;
+
+ case 'RSA-SHA1':
+ if (!isset($credentials['privateKey'])) {
+ return $request;
+ }
+ $value = $this->_rsaSha1($request, $credentials);
+ break;
+
+ case 'PLAINTEXT':
+ $hasKeys = isset(
+ $credentials['consumerSecret'],
+ $credentials['token'],
+ $credentials['tokenSecret']
+ );
+ if (!$hasKeys) {
+ return $request;
+ }
+ $value = $this->_plaintext($request, $credentials);
+ break;
+
+ default:
+ throw new CakeException(sprintf('Unknown Oauth signature method %s', $credentials['method']));
+ }
+
+ return $request->withHeader('Authorization', $value);
+ }
+
+ /**
+ * Plaintext signing
+ *
+ * This method is **not** suitable for plain HTTP.
+ * You should only ever use PLAINTEXT when dealing with SSL
+ * services.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $credentials Authentication credentials.
+ * @return string Authorization header.
+ */
+ protected function _plaintext(Request $request, array $credentials): string
+ {
+ $values = [
+ 'oauth_version' => '1.0',
+ 'oauth_nonce' => uniqid(),
+ 'oauth_timestamp' => time(),
+ 'oauth_signature_method' => 'PLAINTEXT',
+ 'oauth_token' => $credentials['token'],
+ 'oauth_consumer_key' => $credentials['consumerKey'],
+ ];
+ if (isset($credentials['realm'])) {
+ $values['oauth_realm'] = $credentials['realm'];
+ }
+ $key = [$credentials['consumerSecret'], $credentials['tokenSecret']];
+ $key = implode('&', $key);
+ $values['oauth_signature'] = $key;
+
+ return $this->_buildAuth($values);
+ }
+
+ /**
+ * Use HMAC-SHA1 signing.
+ *
+ * This method is suitable for plain HTTP or HTTPS.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $credentials Authentication credentials.
+ * @return string
+ */
+ protected function _hmacSha1(Request $request, array $credentials): string
+ {
+ $nonce = $credentials['nonce'] ?? uniqid();
+ $timestamp = $credentials['timestamp'] ?? time();
+ $values = [
+ 'oauth_version' => '1.0',
+ 'oauth_nonce' => $nonce,
+ 'oauth_timestamp' => $timestamp,
+ 'oauth_signature_method' => 'HMAC-SHA1',
+ 'oauth_token' => $credentials['token'],
+ 'oauth_consumer_key' => $this->_encode($credentials['consumerKey']),
+ ];
+ $baseString = $this->baseString($request, $values);
+
+ // Consumer key should only be encoded for base string calculation as
+ // auth header generation already encodes independently
+ $values['oauth_consumer_key'] = $credentials['consumerKey'];
+
+ if (isset($credentials['realm'])) {
+ $values['oauth_realm'] = $credentials['realm'];
+ }
+ $key = [$credentials['consumerSecret'], $credentials['tokenSecret']];
+ $key = array_map([$this, '_encode'], $key);
+ $key = implode('&', $key);
+
+ $values['oauth_signature'] = base64_encode(
+ hash_hmac('sha1', $baseString, $key, true)
+ );
+
+ return $this->_buildAuth($values);
+ }
+
+ /**
+ * Use RSA-SHA1 signing.
+ *
+ * This method is suitable for plain HTTP or HTTPS.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $credentials Authentication credentials.
+ * @return string
+ * @throws \RuntimeException
+ */
+ protected function _rsaSha1(Request $request, array $credentials): string
+ {
+ if (!function_exists('openssl_pkey_get_private')) {
+ throw new RuntimeException('RSA-SHA1 signature method requires the OpenSSL extension.');
+ }
+
+ $nonce = $credentials['nonce'] ?? bin2hex(Security::randomBytes(16));
+ $timestamp = $credentials['timestamp'] ?? time();
+ $values = [
+ 'oauth_version' => '1.0',
+ 'oauth_nonce' => $nonce,
+ 'oauth_timestamp' => $timestamp,
+ 'oauth_signature_method' => 'RSA-SHA1',
+ 'oauth_consumer_key' => $credentials['consumerKey'],
+ ];
+ if (isset($credentials['consumerSecret'])) {
+ $values['oauth_consumer_secret'] = $credentials['consumerSecret'];
+ }
+ if (isset($credentials['token'])) {
+ $values['oauth_token'] = $credentials['token'];
+ }
+ if (isset($credentials['tokenSecret'])) {
+ $values['oauth_token_secret'] = $credentials['tokenSecret'];
+ }
+ $baseString = $this->baseString($request, $values);
+
+ if (isset($credentials['realm'])) {
+ $values['oauth_realm'] = $credentials['realm'];
+ }
+
+ if (is_resource($credentials['privateKey'])) {
+ $resource = $credentials['privateKey'];
+ $privateKey = stream_get_contents($resource);
+ rewind($resource);
+ $credentials['privateKey'] = $privateKey;
+ }
+
+ $credentials += [
+ 'privateKeyPassphrase' => '',
+ ];
+ if (is_resource($credentials['privateKeyPassphrase'])) {
+ $resource = $credentials['privateKeyPassphrase'];
+ $passphrase = stream_get_line($resource, 0, PHP_EOL);
+ rewind($resource);
+ $credentials['privateKeyPassphrase'] = $passphrase;
+ }
+ $privateKey = openssl_pkey_get_private($credentials['privateKey'], $credentials['privateKeyPassphrase']);
+ $signature = '';
+ openssl_sign($baseString, $signature, $privateKey);
+ if (PHP_MAJOR_VERSION < 8) {
+ openssl_free_key($privateKey);
+ }
+
+ $values['oauth_signature'] = base64_encode($signature);
+
+ return $this->_buildAuth($values);
+ }
+
+ /**
+ * Generate the Oauth basestring
+ *
+ * - Querystring, request data and oauth_* parameters are combined.
+ * - Values are sorted by name and then value.
+ * - Request values are concatenated and urlencoded.
+ * - The request URL (without querystring) is normalized.
+ * - The HTTP method, URL and request parameters are concatenated and returned.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $oauthValues Oauth values.
+ * @return string
+ */
+ public function baseString(Request $request, array $oauthValues): string
+ {
+ $parts = [
+ $request->getMethod(),
+ $this->_normalizedUrl($request->getUri()),
+ $this->_normalizedParams($request, $oauthValues),
+ ];
+ $parts = array_map([$this, '_encode'], $parts);
+
+ return implode('&', $parts);
+ }
+
+ /**
+ * Builds a normalized URL
+ *
+ * Section 9.1.2. of the Oauth spec
+ *
+ * @param \Psr\Http\Message\UriInterface $uri Uri object to build a normalized version of.
+ * @return string Normalized URL
+ */
+ protected function _normalizedUrl(UriInterface $uri): string
+ {
+ $out = $uri->getScheme() . '://';
+ $out .= strtolower($uri->getHost());
+ $out .= $uri->getPath();
+
+ return $out;
+ }
+
+ /**
+ * Sorts and normalizes request data and oauthValues
+ *
+ * Section 9.1.1 of Oauth spec.
+ *
+ * - URL encode keys + values.
+ * - Sort keys & values by byte value.
+ *
+ * @param \Cake\Http\Client\Request $request The request object.
+ * @param array $oauthValues Oauth values.
+ * @return string sorted and normalized values
+ */
+ protected function _normalizedParams(Request $request, array $oauthValues): string
+ {
+ $query = parse_url((string)$request->getUri(), PHP_URL_QUERY);
+ parse_str((string)$query, $queryArgs);
+
+ $post = [];
+ $contentType = $request->getHeaderLine('Content-Type');
+ if ($contentType === '' || $contentType === 'application/x-www-form-urlencoded') {
+ parse_str((string)$request->getBody(), $post);
+ }
+ $args = array_merge($queryArgs, $oauthValues, $post);
+ $pairs = $this->_normalizeData($args);
+ $data = [];
+ foreach ($pairs as $pair) {
+ $data[] = implode('=', $pair);
+ }
+ sort($data, SORT_STRING);
+
+ return implode('&', $data);
+ }
+
+ /**
+ * Recursively convert request data into the normalized form.
+ *
+ * @param array $args The arguments to normalize.
+ * @param string $path The current path being converted.
+ * @see https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+ * @return array
+ */
+ protected function _normalizeData(array $args, string $path = ''): array
+ {
+ $data = [];
+ foreach ($args as $key => $value) {
+ if ($path) {
+ // Fold string keys with [].
+ // Numeric keys result in a=b&a=c. While this isn't
+ // standard behavior in PHP, it is common in other platforms.
+ if (!is_numeric($key)) {
+ $key = "{$path}[{$key}]";
+ } else {
+ $key = $path;
+ }
+ }
+ if (is_array($value)) {
+ uksort($value, 'strcmp');
+ $data = array_merge($data, $this->_normalizeData($value, $key));
+ } else {
+ $data[] = [$key, $value];
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Builds the Oauth Authorization header value.
+ *
+ * @param array $data The oauth_* values to build
+ * @return string
+ */
+ protected function _buildAuth(array $data): string
+ {
+ $out = 'OAuth ';
+ $params = [];
+ foreach ($data as $key => $value) {
+ $params[] = $key . '="' . $this->_encode((string)$value) . '"';
+ }
+ $out .= implode(',', $params);
+
+ return $out;
+ }
+
+ /**
+ * URL Encodes a value based on rules of rfc3986
+ *
+ * @param string $value Value to encode.
+ * @return string
+ */
+ protected function _encode(string $value): string
+ {
+ return str_replace(['%7E', '+'], ['~', ' '], rawurlencode($value));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Exception/ClientException.php b/app/vendor/cakephp/cakephp/src/Http/Client/Exception/ClientException.php
new file mode 100644
index 000000000..c3f77459a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Exception/ClientException.php
@@ -0,0 +1,26 @@
+request = $request;
+ parent::__construct($message, 0, $previous);
+ }
+
+ /**
+ * Returns the request.
+ *
+ * The request object MAY be a different object from the one passed to ClientInterface::sendRequest()
+ *
+ * @return \Psr\Http\Message\RequestInterface
+ */
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Exception/RequestException.php b/app/vendor/cakephp/cakephp/src/Http/Client/Exception/RequestException.php
new file mode 100644
index 000000000..ab8b06874
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Exception/RequestException.php
@@ -0,0 +1,62 @@
+request = $request;
+ parent::__construct($message, 0, $previous);
+ }
+
+ /**
+ * Returns the request.
+ *
+ * The request object MAY be a different object from the one passed to ClientInterface::sendRequest()
+ *
+ * @return \Psr\Http\Message\RequestInterface
+ */
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/FormData.php b/app/vendor/cakephp/cakephp/src/Http/Client/FormData.php
new file mode 100644
index 000000000..0e87c1fbf
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/FormData.php
@@ -0,0 +1,267 @@
+_boundary) {
+ return $this->_boundary;
+ }
+ $this->_boundary = md5(uniqid((string)time()));
+
+ return $this->_boundary;
+ }
+
+ /**
+ * Method for creating new instances of Part
+ *
+ * @param string $name The name of the part.
+ * @param string $value The value to add.
+ * @return \Cake\Http\Client\FormDataPart
+ */
+ public function newPart(string $name, string $value): FormDataPart
+ {
+ return new FormDataPart($name, $value);
+ }
+
+ /**
+ * Add a new part to the data.
+ *
+ * The value for a part can be a string, array, int,
+ * float, filehandle, or object implementing __toString()
+ *
+ * If the $value is an array, multiple parts will be added.
+ * Files will be read from their current position and saved in memory.
+ *
+ * @param string|\Cake\Http\Client\FormDataPart $name The name of the part to add,
+ * or the part data object.
+ * @param mixed $value The value for the part.
+ * @return $this
+ */
+ public function add($name, $value = null)
+ {
+ if (is_string($name)) {
+ if (is_array($value)) {
+ $this->addRecursive($name, $value);
+ } elseif (is_resource($value)) {
+ $this->addFile($name, $value);
+ } else {
+ $this->_parts[] = $this->newPart($name, (string)$value);
+ }
+ } else {
+ $this->_hasComplexPart = true;
+ $this->_parts[] = $name;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add multiple parts at once.
+ *
+ * Iterates the parameter and adds all the key/values.
+ *
+ * @param array $data Array of data to add.
+ * @return $this
+ */
+ public function addMany(array $data)
+ {
+ foreach ($data as $name => $value) {
+ $this->add($name, $value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add either a file reference (string starting with @)
+ * or a file handle.
+ *
+ * @param string $name The name to use.
+ * @param mixed $value Either a string filename, or a filehandle.
+ * @return \Cake\Http\Client\FormDataPart
+ */
+ public function addFile(string $name, $value): FormDataPart
+ {
+ $this->_hasFile = true;
+
+ $filename = false;
+ $contentType = 'application/octet-stream';
+ if (is_resource($value)) {
+ $content = stream_get_contents($value);
+ if (stream_is_local($value)) {
+ $finfo = new finfo(FILEINFO_MIME);
+ $metadata = stream_get_meta_data($value);
+ $contentType = $finfo->file($metadata['uri']);
+ $filename = basename($metadata['uri']);
+ }
+ } else {
+ $finfo = new finfo(FILEINFO_MIME);
+ $value = substr($value, 1);
+ $filename = basename($value);
+ $content = file_get_contents($value);
+ $contentType = $finfo->file($value);
+ }
+ $part = $this->newPart($name, $content);
+ $part->type($contentType);
+ if ($filename) {
+ $part->filename($filename);
+ }
+ $this->add($part);
+
+ return $part;
+ }
+
+ /**
+ * Recursively add data.
+ *
+ * @param string $name The name to use.
+ * @param mixed $value The value to add.
+ * @return void
+ */
+ public function addRecursive(string $name, $value): void
+ {
+ foreach ($value as $key => $value) {
+ $key = $name . '[' . $key . ']';
+ $this->add($key, $value);
+ }
+ }
+
+ /**
+ * Returns the count of parts inside this object.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->_parts);
+ }
+
+ /**
+ * Check whether or not the current payload
+ * has any files.
+ *
+ * @return bool Whether or not there is a file in this payload.
+ */
+ public function hasFile(): bool
+ {
+ return $this->_hasFile;
+ }
+
+ /**
+ * Check whether or not the current payload
+ * is multipart.
+ *
+ * A payload will become multipart when you add files
+ * or use add() with a Part instance.
+ *
+ * @return bool Whether or not the payload is multipart.
+ */
+ public function isMultipart(): bool
+ {
+ return $this->hasFile() || $this->_hasComplexPart;
+ }
+
+ /**
+ * Get the content type for this payload.
+ *
+ * If this object contains files, `multipart/form-data` will be used,
+ * otherwise `application/x-www-form-urlencoded` will be used.
+ *
+ * @return string
+ */
+ public function contentType(): string
+ {
+ if (!$this->isMultipart()) {
+ return 'application/x-www-form-urlencoded';
+ }
+
+ return 'multipart/form-data; boundary=' . $this->boundary();
+ }
+
+ /**
+ * Converts the FormData and its parts into a string suitable
+ * for use in an HTTP request.
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ if ($this->isMultipart()) {
+ $boundary = $this->boundary();
+ $out = '';
+ foreach ($this->_parts as $part) {
+ $out .= "--$boundary\r\n";
+ $out .= (string)$part;
+ $out .= "\r\n";
+ }
+ $out .= "--$boundary--\r\n";
+
+ return $out;
+ }
+ $data = [];
+ foreach ($this->_parts as $part) {
+ $data[$part->name()] = $part->value();
+ }
+
+ return http_build_query($data);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/FormDataPart.php b/app/vendor/cakephp/cakephp/src/Http/Client/FormDataPart.php
new file mode 100644
index 000000000..002c9ca6f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/FormDataPart.php
@@ -0,0 +1,261 @@
+_name = $name;
+ $this->_value = $value;
+ $this->_disposition = $disposition;
+ $this->_charset = $charset;
+ }
+
+ /**
+ * Get/set the disposition type
+ *
+ * By passing in `false` you can disable the disposition
+ * header from being added.
+ *
+ * @param string|null $disposition Use null to get/string to set.
+ * @return string
+ */
+ public function disposition(?string $disposition = null): string
+ {
+ if ($disposition === null) {
+ return $this->_disposition;
+ }
+
+ return $this->_disposition = $disposition;
+ }
+
+ /**
+ * Get/set the contentId for a part.
+ *
+ * @param string|null $id The content id.
+ * @return string|null
+ */
+ public function contentId(?string $id = null): ?string
+ {
+ if ($id === null) {
+ return $this->_contentId;
+ }
+
+ return $this->_contentId = $id;
+ }
+
+ /**
+ * Get/set the filename.
+ *
+ * Setting the filename to `false` will exclude it from the
+ * generated output.
+ *
+ * @param string|null $filename Use null to get/string to set.
+ * @return string|null
+ */
+ public function filename(?string $filename = null): ?string
+ {
+ if ($filename === null) {
+ return $this->_filename;
+ }
+
+ return $this->_filename = $filename;
+ }
+
+ /**
+ * Get/set the content type.
+ *
+ * @param string|null $type Use null to get/string to set.
+ * @return string|null
+ */
+ public function type(?string $type): ?string
+ {
+ if ($type === null) {
+ return $this->_type;
+ }
+
+ return $this->_type = $type;
+ }
+
+ /**
+ * Set the transfer-encoding for multipart.
+ *
+ * Useful when content bodies are in encodings like base64.
+ *
+ * @param string|null $type The type of encoding the value has.
+ * @return string|null
+ */
+ public function transferEncoding(?string $type): ?string
+ {
+ if ($type === null) {
+ return $this->_transferEncoding;
+ }
+
+ return $this->_transferEncoding = $type;
+ }
+
+ /**
+ * Get the part name.
+ *
+ * @return string
+ */
+ public function name(): string
+ {
+ return $this->_name;
+ }
+
+ /**
+ * Get the value.
+ *
+ * @return string
+ */
+ public function value(): string
+ {
+ return $this->_value;
+ }
+
+ /**
+ * Convert the part into a string.
+ *
+ * Creates a string suitable for use in HTTP requests.
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ $out = '';
+ if ($this->_disposition) {
+ $out .= 'Content-Disposition: ' . $this->_disposition;
+ if ($this->_name) {
+ $out .= '; ' . $this->_headerParameterToString('name', $this->_name);
+ }
+ if ($this->_filename) {
+ $out .= '; ' . $this->_headerParameterToString('filename', $this->_filename);
+ }
+ $out .= "\r\n";
+ }
+ if ($this->_type) {
+ $out .= 'Content-Type: ' . $this->_type . "\r\n";
+ }
+ if ($this->_transferEncoding) {
+ $out .= 'Content-Transfer-Encoding: ' . $this->_transferEncoding . "\r\n";
+ }
+ if ($this->_contentId) {
+ $out .= 'Content-ID: <' . $this->_contentId . ">\r\n";
+ }
+ $out .= "\r\n";
+ $out .= $this->_value;
+
+ return $out;
+ }
+
+ /**
+ * Get the string for the header parameter.
+ *
+ * If the value contains non-ASCII letters an additional header indicating
+ * the charset encoding will be set.
+ *
+ * @param string $name The name of the header parameter
+ * @param string $value The value of the header parameter
+ * @return string
+ */
+ protected function _headerParameterToString(string $name, string $value): string
+ {
+ $transliterated = Text::transliterate(str_replace('"', '', $value));
+ $return = sprintf('%s="%s"', $name, $transliterated);
+ if ($this->_charset !== null && $value !== $transliterated) {
+ $return .= sprintf("; %s*=%s''%s", $name, strtolower($this->_charset), rawurlencode($value));
+ }
+
+ return $return;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Message.php b/app/vendor/cakephp/cakephp/src/Http/Client/Message.php
new file mode 100644
index 000000000..a23282d20
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Message.php
@@ -0,0 +1,161 @@
+_cookies;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Request.php b/app/vendor/cakephp/cakephp/src/Http/Client/Request.php
new file mode 100644
index 000000000..9ae13cb08
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Request.php
@@ -0,0 +1,98 @@
+setMethod($method);
+ $this->uri = $this->createUri($url);
+ $headers += [
+ 'Connection' => 'close',
+ 'User-Agent' => 'CakePHP',
+ ];
+ $this->addHeaders($headers);
+
+ if ($data === null) {
+ $this->stream = new Stream('php://memory', 'rw');
+ } else {
+ $this->setContent($data);
+ }
+ }
+
+ /**
+ * Add an array of headers to the request.
+ *
+ * @param array $headers The headers to add.
+ * @return void
+ */
+ protected function addHeaders(array $headers): void
+ {
+ foreach ($headers as $key => $val) {
+ $normalized = strtolower($key);
+ $this->headers[$key] = (array)$val;
+ $this->headerNames[$normalized] = $key;
+ }
+ }
+
+ /**
+ * Set the body/payload for the message.
+ *
+ * Array data will be serialized with Cake\Http\FormData,
+ * and the content-type will be set.
+ *
+ * @param string|array $content The body for the request.
+ * @return $this
+ */
+ protected function setContent($content)
+ {
+ if (is_array($content)) {
+ $formData = new FormData();
+ $formData->addMany($content);
+ $this->addHeaders(['Content-Type' => $formData->contentType()]);
+ $content = (string)$formData;
+ }
+
+ $stream = new Stream('php://memory', 'rw');
+ $stream->write($content);
+ $this->stream = $stream;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Client/Response.php b/app/vendor/cakephp/cakephp/src/Http/Client/Response.php
new file mode 100644
index 000000000..e89ff2693
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Client/Response.php
@@ -0,0 +1,480 @@
+getHeaderLine('content-type');
+ * ```
+ *
+ * Will read the Content-Type header. You can get all set
+ * headers using:
+ *
+ * ```
+ * $response->getHeaders();
+ * ```
+ *
+ * ### Get the response body
+ *
+ * You can access the response body stream using:
+ *
+ * ```
+ * $content = $response->getBody();
+ * ```
+ *
+ * You can get the body string using:
+ *
+ * ```
+ * $content = $response->getStringBody();
+ * ```
+ *
+ * If your response body is in XML or JSON you can use
+ * special content type specific accessors to read the decoded data.
+ * JSON data will be returned as arrays, while XML data will be returned
+ * as SimpleXML nodes:
+ *
+ * ```
+ * // Get as XML
+ * $content = $response->getXml()
+ * // Get as JSON
+ * $content = $response->getJson()
+ * ```
+ *
+ * If the response cannot be decoded, null will be returned.
+ *
+ * ### Check the status code
+ *
+ * You can access the response status code using:
+ *
+ * ```
+ * $content = $response->getStatusCode();
+ * ```
+ */
+class Response extends Message implements ResponseInterface
+{
+ use MessageTrait;
+
+ /**
+ * The status code of the response.
+ *
+ * @var int
+ */
+ protected $code;
+
+ /**
+ * Cookie Collection instance
+ *
+ * @var \Cake\Http\Cookie\CookieCollection
+ */
+ protected $cookies;
+
+ /**
+ * The reason phrase for the status code
+ *
+ * @var string
+ */
+ protected $reasonPhrase;
+
+ /**
+ * Cached decoded XML data.
+ *
+ * @var \SimpleXMLElement
+ */
+ protected $_xml;
+
+ /**
+ * Cached decoded JSON data.
+ *
+ * @var array
+ */
+ protected $_json;
+
+ /**
+ * Constructor
+ *
+ * @param array $headers Unparsed headers.
+ * @param string $body The response body.
+ */
+ public function __construct(array $headers = [], string $body = '')
+ {
+ $this->_parseHeaders($headers);
+ if ($this->getHeaderLine('Content-Encoding') === 'gzip') {
+ $body = $this->_decodeGzipBody($body);
+ }
+ $stream = new Stream('php://memory', 'wb+');
+ $stream->write($body);
+ $stream->rewind();
+ $this->stream = $stream;
+ }
+
+ /**
+ * Uncompress a gzip response.
+ *
+ * Looks for gzip signatures, and if gzinflate() exists,
+ * the body will be decompressed.
+ *
+ * @param string $body Gzip encoded body.
+ * @return string
+ * @throws \RuntimeException When attempting to decode gzip content without gzinflate.
+ */
+ protected function _decodeGzipBody(string $body): string
+ {
+ if (!function_exists('gzinflate')) {
+ throw new RuntimeException('Cannot decompress gzip response body without gzinflate()');
+ }
+ $offset = 0;
+ // Look for gzip 'signature'
+ if (substr($body, 0, 2) === "\x1f\x8b") {
+ $offset = 2;
+ }
+ // Check the format byte
+ if (substr($body, $offset, 1) === "\x08") {
+ return gzinflate(substr($body, $offset + 8));
+ }
+
+ throw new RuntimeException('Invalid gzip response');
+ }
+
+ /**
+ * Parses headers if necessary.
+ *
+ * - Decodes the status code and reasonphrase.
+ * - Parses and normalizes header names + values.
+ *
+ * @param array $headers Headers to parse.
+ * @return void
+ */
+ protected function _parseHeaders(array $headers): void
+ {
+ foreach ($headers as $value) {
+ if (substr($value, 0, 5) === 'HTTP/') {
+ preg_match('/HTTP\/([\d.]+) ([0-9]+)(.*)/i', $value, $matches);
+ $this->protocol = $matches[1];
+ $this->code = (int)$matches[2];
+ $this->reasonPhrase = trim($matches[3]);
+ continue;
+ }
+ if (strpos($value, ':') === false) {
+ continue;
+ }
+ [$name, $value] = explode(':', $value, 2);
+ $value = trim($value);
+ $name = trim($name);
+
+ $normalized = strtolower($name);
+
+ if (isset($this->headers[$name])) {
+ $this->headers[$name][] = $value;
+ } else {
+ $this->headers[$name] = (array)$value;
+ $this->headerNames[$normalized] = $name;
+ }
+ }
+ }
+
+ /**
+ * Check if the response status code was in the 2xx/3xx range
+ *
+ * @return bool
+ */
+ public function isOk(): bool
+ {
+ return $this->code >= 200 && $this->code <= 399;
+ }
+
+ /**
+ * Check if the response status code was in the 2xx range
+ *
+ * @return bool
+ */
+ public function isSuccess(): bool
+ {
+ return $this->code >= 200 && $this->code <= 299;
+ }
+
+ /**
+ * Check if the response had a redirect status code.
+ *
+ * @return bool
+ */
+ public function isRedirect(): bool
+ {
+ $codes = [
+ static::STATUS_MOVED_PERMANENTLY,
+ static::STATUS_FOUND,
+ static::STATUS_SEE_OTHER,
+ static::STATUS_TEMPORARY_REDIRECT,
+ ];
+
+ return in_array($this->code, $codes, true) &&
+ $this->getHeaderLine('Location');
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return int The status code.
+ */
+ public function getStatusCode(): int
+ {
+ return $this->code;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param int $code The status code to set.
+ * @param string $reasonPhrase The status reason phrase.
+ * @return static A copy of the current object with an updated status code.
+ */
+ public function withStatus($code, $reasonPhrase = '')
+ {
+ $new = clone $this;
+ $new->code = $code;
+ $new->reasonPhrase = $reasonPhrase;
+
+ return $new;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return string The current reason phrase.
+ */
+ public function getReasonPhrase(): string
+ {
+ return $this->reasonPhrase;
+ }
+
+ /**
+ * Get the encoding if it was set.
+ *
+ * @return string|null
+ */
+ public function getEncoding(): ?string
+ {
+ $content = $this->getHeaderLine('content-type');
+ if (!$content) {
+ return null;
+ }
+ preg_match('/charset\s?=\s?[\'"]?([a-z0-9-_]+)[\'"]?/i', $content, $matches);
+ if (empty($matches[1])) {
+ return null;
+ }
+
+ return $matches[1];
+ }
+
+ /**
+ * Get the all cookie data.
+ *
+ * @return array The cookie data
+ */
+ public function getCookies(): array
+ {
+ return $this->_getCookies();
+ }
+
+ /**
+ * Get the cookie collection from this response.
+ *
+ * This method exposes the response's CookieCollection
+ * instance allowing you to interact with cookie objects directly.
+ *
+ * @return \Cake\Http\Cookie\CookieCollection
+ */
+ public function getCookieCollection(): CookieCollection
+ {
+ $this->buildCookieCollection();
+
+ return $this->cookies;
+ }
+
+ /**
+ * Get the value of a single cookie.
+ *
+ * @param string $name The name of the cookie value.
+ * @return string|array|null Either the cookie's value or null when the cookie is undefined.
+ */
+ public function getCookie(string $name)
+ {
+ $this->buildCookieCollection();
+
+ if (!$this->cookies->has($name)) {
+ return null;
+ }
+
+ return $this->cookies->get($name)->getValue();
+ }
+
+ /**
+ * Get the full data for a single cookie.
+ *
+ * @param string $name The name of the cookie value.
+ * @return array|null Either the cookie's data or null when the cookie is undefined.
+ */
+ public function getCookieData(string $name): ?array
+ {
+ $this->buildCookieCollection();
+
+ if (!$this->cookies->has($name)) {
+ return null;
+ }
+
+ return $this->cookies->get($name)->toArray();
+ }
+
+ /**
+ * Lazily build the CookieCollection and cookie objects from the response header
+ *
+ * @return void
+ */
+ protected function buildCookieCollection(): void
+ {
+ if ($this->cookies !== null) {
+ return;
+ }
+ $this->cookies = CookieCollection::createFromHeader($this->getHeader('Set-Cookie'));
+ }
+
+ /**
+ * Property accessor for `$this->cookies`
+ *
+ * @return array Array of Cookie data.
+ */
+ protected function _getCookies(): array
+ {
+ $this->buildCookieCollection();
+
+ $out = [];
+ /** @var \Cake\Http\Cookie\Cookie[] $cookies */
+ $cookies = $this->cookies;
+ foreach ($cookies as $cookie) {
+ $out[$cookie->getName()] = $cookie->toArray();
+ }
+
+ return $out;
+ }
+
+ /**
+ * Get the response body as string.
+ *
+ * @return string
+ */
+ public function getStringBody(): string
+ {
+ return $this->_getBody();
+ }
+
+ /**
+ * Get the response body as JSON decoded data.
+ *
+ * @return mixed
+ */
+ public function getJson()
+ {
+ return $this->_getJson();
+ }
+
+ /**
+ * Get the response body as JSON decoded data.
+ *
+ * @return mixed
+ */
+ protected function _getJson()
+ {
+ if ($this->_json) {
+ return $this->_json;
+ }
+
+ return $this->_json = json_decode($this->_getBody(), true);
+ }
+
+ /**
+ * Get the response body as XML decoded data.
+ *
+ * @return \SimpleXMLElement|null
+ */
+ public function getXml(): ?SimpleXMLElement
+ {
+ return $this->_getXml();
+ }
+
+ /**
+ * Get the response body as XML decoded data.
+ *
+ * @return \SimpleXMLElement|null
+ */
+ protected function _getXml(): ?SimpleXMLElement
+ {
+ if ($this->_xml !== null) {
+ return $this->_xml;
+ }
+ libxml_use_internal_errors();
+ $data = simplexml_load_string($this->_getBody());
+ if ($data) {
+ $this->_xml = $data;
+
+ return $this->_xml;
+ }
+
+ return null;
+ }
+
+ /**
+ * Provides magic __get() support.
+ *
+ * @return string[]
+ */
+ protected function _getHeaders(): array
+ {
+ $out = [];
+ foreach ($this->headers as $key => $values) {
+ $out[$key] = implode(',', $values);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Provides magic __get() support.
+ *
+ * @return string
+ */
+ protected function _getBody(): string
+ {
+ $this->stream->rewind();
+
+ return $this->stream->getContents();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/ControllerFactory.php b/app/vendor/cakephp/cakephp/src/Http/ControllerFactory.php
new file mode 100644
index 000000000..4e01026b8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/ControllerFactory.php
@@ -0,0 +1,10 @@
+withValue('0');
+ * ```
+ *
+ * @link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03
+ * @link https://en.wikipedia.org/wiki/HTTP_cookie
+ * @see \Cake\Http\Cookie\CookieCollection for working with collections of cookies.
+ * @see \Cake\Http\Response::getCookieCollection() for working with response cookies.
+ */
+class Cookie implements CookieInterface
+{
+ /**
+ * Cookie name
+ *
+ * @var string
+ */
+ protected $name = '';
+
+ /**
+ * Raw Cookie value.
+ *
+ * @var string|array
+ */
+ protected $value = '';
+
+ /**
+ * Whether or not a JSON value has been expanded into an array.
+ *
+ * @var bool
+ */
+ protected $isExpanded = false;
+
+ /**
+ * Expiration time
+ *
+ * @var \DateTime|\DateTimeImmutable|null
+ */
+ protected $expiresAt;
+
+ /**
+ * Path
+ *
+ * @var string
+ */
+ protected $path = '/';
+
+ /**
+ * Domain
+ *
+ * @var string
+ */
+ protected $domain = '';
+
+ /**
+ * Secure
+ *
+ * @var bool
+ */
+ protected $secure = false;
+
+ /**
+ * HTTP only
+ *
+ * @var bool
+ */
+ protected $httpOnly = false;
+
+ /**
+ * Samesite
+ *
+ * @var string|null
+ */
+ protected $sameSite = null;
+
+ /**
+ * Default attributes for a cookie.
+ *
+ * @var array
+ * @see \Cake\Cookie\Cookie::setDefaults()
+ */
+ protected static $defaults = [
+ 'expires' => null,
+ 'path' => '/',
+ 'domain' => '',
+ 'secure' => false,
+ 'httponly' => false,
+ 'samesite' => null,
+ ];
+
+ /**
+ * Constructor
+ *
+ * The constructors args are similar to the native PHP `setcookie()` method.
+ * The only difference is the 3rd argument which excepts null or an
+ * DateTime or DateTimeImmutable object instead an integer.
+ *
+ * @link http://php.net/manual/en/function.setcookie.php
+ * @param string $name Cookie name
+ * @param string|array $value Value of the cookie
+ * @param \DateTime|\DateTimeImmutable|null $expiresAt Expiration time and date
+ * @param string|null $path Path
+ * @param string|null $domain Domain
+ * @param bool|null $secure Is secure
+ * @param bool|null $httpOnly HTTP Only
+ * @param string|null $sameSite Samesite
+ */
+ public function __construct(
+ string $name,
+ $value = '',
+ ?DateTimeInterface $expiresAt = null,
+ ?string $path = null,
+ ?string $domain = null,
+ ?bool $secure = null,
+ ?bool $httpOnly = null,
+ ?string $sameSite = null
+ ) {
+ $this->validateName($name);
+ $this->name = $name;
+
+ $this->_setValue($value);
+
+ $this->domain = $domain ?? static::$defaults['domain'];
+ $this->httpOnly = $httpOnly ?? static::$defaults['httponly'];
+ $this->path = $path ?? static::$defaults['path'];
+ $this->secure = $secure ?? static::$defaults['secure'];
+ if ($sameSite === null) {
+ $this->sameSite = static::$defaults['samesite'];
+ } else {
+ $this->validateSameSiteValue($sameSite);
+ $this->sameSite = $sameSite;
+ }
+
+ if ($expiresAt) {
+ $expiresAt = $expiresAt->setTimezone(new DateTimeZone('GMT'));
+ } else {
+ $expiresAt = static::$defaults['expires'];
+ }
+ $this->expiresAt = $expiresAt;
+ }
+
+ /**
+ * Set default options for the cookies.
+ *
+ * Valid option keys are:
+ *
+ * - `expires`: Can be a UNIX timestamp or `strtotime()` compatible string or `DateTimeInterface` instance or `null`.
+ * - `path`: A path string. Defauts to `'/'`.
+ * - `domain`: Domain name string. Defaults to `''`.
+ * - `httponly`: Boolean. Defaults to `false`.
+ * - `secure`: Boolean. Defaults to `false`.
+ * - `samesite`: Can be one of `CookieInterface::SAMESITE_LAX`, `CookieInterface::SAMESITE_STRICT`,
+ * `CookieInterface::SAMESITE_NONE` or `null`. Defaults to `null`.
+ *
+ * @param array $options Default options.
+ * @return void
+ */
+ public static function setDefaults(array $options): void
+ {
+ if (isset($options['expires'])) {
+ $options['expires'] = static::dateTimeInstance($options['expires']);
+ }
+ if (isset($options['samesite'])) {
+ static::validateSameSiteValue($options['samesite']);
+ }
+
+ static::$defaults = $options + static::$defaults;
+ }
+
+ /**
+ * Factory method to create Cookie instances.
+ *
+ * @param string $name Cookie name
+ * @param string|array $value Value of the cookie
+ * @param array $options Cookies options.
+ * @return static
+ * @see \Cake\Cookie\Cookie::setDefaults()
+ */
+ public static function create(string $name, $value, array $options = [])
+ {
+ $options += static::$defaults;
+ $options['expires'] = static::dateTimeInstance($options['expires']);
+
+ return new static(
+ $name,
+ $value,
+ $options['expires'],
+ $options['path'],
+ $options['domain'],
+ $options['secure'],
+ $options['httponly'],
+ $options['samesite']
+ );
+ }
+
+ /**
+ * Converts non null expiry value into DateTimeInterface instance.
+ *
+ * @param mixed $expires Expiry value.
+ * @return \DateTime|\DatetimeImmutable|null
+ */
+ protected static function dateTimeInstance($expires): ?DateTimeInterface
+ {
+ if ($expires === null) {
+ return $expires;
+ }
+
+ if ($expires instanceof DateTimeInterface) {
+ /** @psalm-suppress UndefinedInterfaceMethod */
+ return $expires->setTimezone(new DateTimeZone('GMT'));
+ }
+
+ if (!is_string($expires) && !is_int($expires)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Invalid type `%s` for expires. Expected an string, integer or DateTime object.',
+ getTypeName($expires)
+ ));
+ }
+
+ if (!is_numeric($expires)) {
+ $expires = strtotime($expires) ?: null;
+ }
+
+ if ($expires !== null) {
+ $expires = new DateTimeImmutable('@' . (string)$expires);
+ }
+
+ return $expires;
+ }
+
+ /**
+ * Create Cookie instance from "set-cookie" header string.
+ *
+ * @param string $cookie Cookie header string.
+ * @param array $defaults Default attributes.
+ * @return static
+ * @see \Cake\Cookie\Cookie::setDefaults()
+ */
+ public static function createFromHeaderString(string $cookie, array $defaults = [])
+ {
+ if (strpos($cookie, '";"') !== false) {
+ $cookie = str_replace('";"', '{__cookie_replace__}', $cookie);
+ $parts = str_replace('{__cookie_replace__}', '";"', explode(';', $cookie));
+ } else {
+ $parts = preg_split('/\;[ \t]*/', $cookie);
+ }
+
+ [$name, $value] = explode('=', array_shift($parts), 2);
+ $data = [
+ 'name' => urldecode($name),
+ 'value' => urldecode($value),
+ ] + $defaults;
+
+ foreach ($parts as $part) {
+ if (strpos($part, '=') !== false) {
+ [$key, $value] = explode('=', $part);
+ } else {
+ $key = $part;
+ $value = true;
+ }
+
+ $key = strtolower($key);
+ $data[$key] = $value;
+ }
+
+ if (isset($data['max-age'])) {
+ $data['expires'] = time() + (int)$data['max-age'];
+ unset($data['max-age']);
+ }
+
+ if (isset($data['samesite'])) {
+ // Ignore invalid value when parsing headers
+ // https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
+ if (!in_array($data['samesite'], CookieInterface::SAMESITE_VALUES, true)) {
+ unset($data['samesite']);
+ }
+ }
+
+ $name = (string)$data['name'];
+ $value = (string)$data['value'];
+ unset($data['name'], $data['value']);
+
+ return Cookie::create(
+ $name,
+ $value,
+ $data
+ );
+ }
+
+ /**
+ * Returns a header value as string
+ *
+ * @return string
+ */
+ public function toHeaderValue(): string
+ {
+ $value = $this->value;
+ if ($this->isExpanded) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $value = $this->_flatten($this->value);
+ }
+ $headerValue = [];
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $headerValue[] = sprintf('%s=%s', $this->name, rawurlencode($value));
+
+ if ($this->expiresAt) {
+ $headerValue[] = sprintf('expires=%s', $this->getFormattedExpires());
+ }
+ if ($this->path !== '') {
+ $headerValue[] = sprintf('path=%s', $this->path);
+ }
+ if ($this->domain !== '') {
+ $headerValue[] = sprintf('domain=%s', $this->domain);
+ }
+ if ($this->sameSite) {
+ $headerValue[] = sprintf('samesite=%s', $this->sameSite);
+ }
+ if ($this->secure) {
+ $headerValue[] = 'secure';
+ }
+ if ($this->httpOnly) {
+ $headerValue[] = 'httponly';
+ }
+
+ return implode('; ', $headerValue);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withName(string $name)
+ {
+ $this->validateName($name);
+ $new = clone $this;
+ $new->name = $name;
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getId(): string
+ {
+ return "{$this->name};{$this->domain};{$this->path}";
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Validates the cookie name
+ *
+ * @param string $name Name of the cookie
+ * @return void
+ * @throws \InvalidArgumentException
+ * @link https://tools.ietf.org/html/rfc2616#section-2.2 Rules for naming cookies.
+ */
+ protected function validateName(string $name): void
+ {
+ if (preg_match("/[=,;\t\r\n\013\014]/", $name)) {
+ throw new InvalidArgumentException(
+ sprintf('The cookie name `%s` contains invalid characters.', $name)
+ );
+ }
+
+ if (empty($name)) {
+ throw new InvalidArgumentException('The cookie name cannot be empty.');
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Gets the cookie value as a string.
+ *
+ * This will collapse any complex data in the cookie with json_encode()
+ *
+ * @return mixed
+ * @deprecated 4.0.0 Use {@link getScalarValue()} instead.
+ */
+ public function getStringValue()
+ {
+ deprecationWarning('Cookie::getStringValue() is deprecated. Use getScalarValue() instead.');
+
+ return $this->getScalarValue();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getScalarValue()
+ {
+ if ($this->isExpanded) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ return $this->_flatten($this->value);
+ }
+
+ return $this->value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withValue($value)
+ {
+ $new = clone $this;
+ $new->_setValue($value);
+
+ return $new;
+ }
+
+ /**
+ * Setter for the value attribute.
+ *
+ * @param string|array $value The value to store.
+ * @return void
+ */
+ protected function _setValue($value): void
+ {
+ $this->isExpanded = is_array($value);
+ $this->value = $value;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withPath(string $path)
+ {
+ $new = clone $this;
+ $new->path = $path;
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withDomain(string $domain)
+ {
+ $new = clone $this;
+ $new->domain = $domain;
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDomain(): string
+ {
+ return $this->domain;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isSecure(): bool
+ {
+ return $this->secure;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withSecure(bool $secure)
+ {
+ $new = clone $this;
+ $new->secure = $secure;
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withHttpOnly(bool $httpOnly)
+ {
+ $new = clone $this;
+ $new->httpOnly = $httpOnly;
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isHttpOnly(): bool
+ {
+ return $this->httpOnly;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withExpiry($dateTime)
+ {
+ $new = clone $this;
+ $new->expiresAt = $dateTime->setTimezone(new DateTimeZone('GMT'));
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getExpiry()
+ {
+ return $this->expiresAt;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getExpiresTimestamp(): ?int
+ {
+ if (!$this->expiresAt) {
+ return null;
+ }
+
+ return (int)$this->expiresAt->format('U');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getFormattedExpires(): string
+ {
+ if (!$this->expiresAt) {
+ return '';
+ }
+
+ return $this->expiresAt->format(static::EXPIRES_FORMAT);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isExpired($time = null): bool
+ {
+ $time = $time ?: new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ if (!$this->expiresAt) {
+ return false;
+ }
+
+ return $this->expiresAt < $time;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withNeverExpire()
+ {
+ $new = clone $this;
+ $new->expiresAt = new DateTimeImmutable('2038-01-01');
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withExpired()
+ {
+ $new = clone $this;
+ $new->expiresAt = new DateTimeImmutable('1970-01-01 00:00:01');
+
+ return $new;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getSameSite(): ?string
+ {
+ return $this->sameSite;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function withSameSite(?string $sameSite)
+ {
+ if ($sameSite !== null) {
+ $this->validateSameSiteValue($sameSite);
+ }
+
+ $new = clone $this;
+ $new->sameSite = $sameSite;
+
+ return $new;
+ }
+
+ /**
+ * Check that value passed for SameSite is valid.
+ *
+ * @param string $sameSite SameSite value
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ protected static function validateSameSiteValue(string $sameSite)
+ {
+ if (!in_array($sameSite, CookieInterface::SAMESITE_VALUES, true)) {
+ throw new InvalidArgumentException(
+ 'Samesite value must be either of: ' . implode(', ', CookieInterface::SAMESITE_VALUES)
+ );
+ }
+ }
+
+ /**
+ * Checks if a value exists in the cookie data.
+ *
+ * This method will expand serialized complex data,
+ * on first use.
+ *
+ * @param string $path Path to check
+ * @return bool
+ */
+ public function check(string $path): bool
+ {
+ if ($this->isExpanded === false) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $this->value = $this->_expand($this->value);
+ }
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ return Hash::check($this->value, $path);
+ }
+
+ /**
+ * Create a new cookie with updated data.
+ *
+ * @param string $path Path to write to
+ * @param mixed $value Value to write
+ * @return static
+ */
+ public function withAddedValue(string $path, $value)
+ {
+ $new = clone $this;
+ if ($new->isExpanded === false) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $new->value = $new->_expand($new->value);
+ }
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $new->value = Hash::insert($new->value, $path, $value);
+
+ return $new;
+ }
+
+ /**
+ * Create a new cookie without a specific path
+ *
+ * @param string $path Path to remove
+ * @return static
+ */
+ public function withoutAddedValue(string $path)
+ {
+ $new = clone $this;
+ if ($new->isExpanded === false) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $new->value = $new->_expand($new->value);
+ }
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $new->value = Hash::remove($new->value, $path);
+
+ return $new;
+ }
+
+ /**
+ * Read data from the cookie
+ *
+ * This method will expand serialized complex data,
+ * on first use.
+ *
+ * @param string $path Path to read the data from
+ * @return mixed
+ */
+ public function read(?string $path = null)
+ {
+ if ($this->isExpanded === false) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $this->value = $this->_expand($this->value);
+ }
+
+ if ($path === null) {
+ return $this->value;
+ }
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ return Hash::get($this->value, $path);
+ }
+
+ /**
+ * Checks if the cookie value was expanded
+ *
+ * @return bool
+ */
+ public function isExpanded(): bool
+ {
+ return $this->isExpanded;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getOptions(): array
+ {
+ $options = [
+ 'expires' => (int)$this->getExpiresTimestamp(),
+ 'path' => $this->path,
+ 'domain' => $this->domain,
+ 'secure' => $this->secure,
+ 'httponly' => $this->httpOnly,
+ ];
+
+ if ($this->sameSite !== null) {
+ $options['samesite'] = $this->sameSite;
+ }
+
+ return $options;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toArray(): array
+ {
+ return [
+ 'name' => $this->name,
+ 'value' => $this->getScalarValue(),
+ ] + $this->getOptions();
+ }
+
+ /**
+ * Implode method to keep keys are multidimensional arrays
+ *
+ * @param array $array Map of key and values
+ * @return string A JSON encoded string.
+ */
+ protected function _flatten(array $array): string
+ {
+ return json_encode($array);
+ }
+
+ /**
+ * Explode method to return array from string set in CookieComponent::_flatten()
+ * Maintains reading backwards compatibility with 1.x CookieComponent::_flatten().
+ *
+ * @param string $string A string containing JSON encoded data, or a bare string.
+ * @return string|array Map of key and values
+ */
+ protected function _expand(string $string)
+ {
+ $this->isExpanded = true;
+ $first = substr($string, 0, 1);
+ if ($first === '{' || $first === '[') {
+ $ret = json_decode($string, true);
+
+ return $ret ?? $string;
+ }
+
+ $array = [];
+ foreach (explode(',', $string) as $pair) {
+ $key = explode('|', $pair);
+ if (!isset($key[1])) {
+ return $key[0];
+ }
+ $array[$key[0]] = $key[1];
+ }
+
+ return $array;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieCollection.php b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieCollection.php
new file mode 100644
index 000000000..23b84be42
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieCollection.php
@@ -0,0 +1,351 @@
+checkCookies($cookies);
+ foreach ($cookies as $cookie) {
+ $this->cookies[$cookie->getId()] = $cookie;
+ }
+ }
+
+ /**
+ * Create a Cookie Collection from an array of Set-Cookie Headers
+ *
+ * @param array $header The array of set-cookie header values.
+ * @param array $defaults The defaults attributes.
+ * @return static
+ */
+ public static function createFromHeader(array $header, array $defaults = [])
+ {
+ $cookies = [];
+ foreach ($header as $value) {
+ try {
+ $cookies[] = Cookie::createFromHeaderString($value, $defaults);
+ } catch (Exception $e) {
+ // Don't blow up on invalid cookies
+ }
+ }
+
+ return new static($cookies);
+ }
+
+ /**
+ * Create a new collection from the cookies in a ServerRequest
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to extract cookie data from
+ * @return static
+ */
+ public static function createFromServerRequest(ServerRequestInterface $request)
+ {
+ $data = $request->getCookieParams();
+ $cookies = [];
+ foreach ($data as $name => $value) {
+ $cookies[] = new Cookie($name, $value);
+ }
+
+ return new static($cookies);
+ }
+
+ /**
+ * Get the number of cookies in the collection.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->cookies);
+ }
+
+ /**
+ * Add a cookie and get an updated collection.
+ *
+ * Cookies are stored by id. This means that there can be duplicate
+ * cookies if a cookie collection is used for cookies across multiple
+ * domains. This can impact how get(), has() and remove() behave.
+ *
+ * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie instance to add.
+ * @return static
+ */
+ public function add(CookieInterface $cookie)
+ {
+ $new = clone $this;
+ $new->cookies[$cookie->getId()] = $cookie;
+
+ return $new;
+ }
+
+ /**
+ * Get the first cookie by name.
+ *
+ * @param string $name The name of the cookie.
+ * @return \Cake\Http\Cookie\CookieInterface
+ * @throws \InvalidArgumentException If cookie not found.
+ */
+ public function get(string $name): CookieInterface
+ {
+ $key = mb_strtolower($name);
+ foreach ($this->cookies as $cookie) {
+ if (mb_strtolower($cookie->getName()) === $key) {
+ return $cookie;
+ }
+ }
+
+ throw new InvalidArgumentException(
+ sprintf(
+ 'Cookie %s not found. Use has() to check first for existence.',
+ $name
+ )
+ );
+ }
+
+ /**
+ * Check if a cookie with the given name exists
+ *
+ * @param string $name The cookie name to check.
+ * @return bool True if the cookie exists, otherwise false.
+ */
+ public function has(string $name): bool
+ {
+ $key = mb_strtolower($name);
+ foreach ($this->cookies as $cookie) {
+ if (mb_strtolower($cookie->getName()) === $key) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Create a new collection with all cookies matching $name removed.
+ *
+ * If the cookie is not in the collection, this method will do nothing.
+ *
+ * @param string $name The name of the cookie to remove.
+ * @return static
+ */
+ public function remove(string $name)
+ {
+ $new = clone $this;
+ $key = mb_strtolower($name);
+ foreach ($new->cookies as $i => $cookie) {
+ if (mb_strtolower($cookie->getName()) === $key) {
+ unset($new->cookies[$i]);
+ }
+ }
+
+ return $new;
+ }
+
+ /**
+ * Checks if only valid cookie objects are in the array
+ *
+ * @param \Cake\Http\Cookie\CookieInterface[] $cookies Array of cookie objects
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ protected function checkCookies(array $cookies): void
+ {
+ foreach ($cookies as $index => $cookie) {
+ if (!$cookie instanceof CookieInterface) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'Expected `%s[]` as $cookies but instead got `%s` at index %d',
+ static::class,
+ getTypeName($cookie),
+ $index
+ )
+ );
+ }
+ }
+ }
+
+ /**
+ * Gets the iterator
+ *
+ * @return \Cake\Http\Cookie\CookieInterface[]
+ * @psalm-return \Traversable
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->cookies);
+ }
+
+ /**
+ * Add cookies that match the path/domain/expiration to the request.
+ *
+ * This allows CookieCollections to be used as a 'cookie jar' in an HTTP client
+ * situation. Cookies that match the request's domain + path that are not expired
+ * when this method is called will be applied to the request.
+ *
+ * @param \Psr\Http\Message\RequestInterface $request The request to update.
+ * @param array $extraCookies Associative array of additional cookies to add into the request. This
+ * is useful when you have cookie data from outside the collection you want to send.
+ * @return \Psr\Http\Message\RequestInterface An updated request.
+ */
+ public function addToRequest(RequestInterface $request, array $extraCookies = []): RequestInterface
+ {
+ $uri = $request->getUri();
+ $cookies = $this->findMatchingCookies(
+ $uri->getScheme(),
+ $uri->getHost(),
+ $uri->getPath() ?: '/'
+ );
+ $cookies = array_merge($cookies, $extraCookies);
+ $cookiePairs = [];
+ foreach ($cookies as $key => $value) {
+ $cookie = sprintf('%s=%s', rawurlencode($key), rawurlencode($value));
+ $size = strlen($cookie);
+ if ($size > 4096) {
+ triggerWarning(sprintf(
+ 'The cookie `%s` exceeds the recommended maximum cookie length of 4096 bytes.',
+ $key
+ ));
+ }
+ $cookiePairs[] = $cookie;
+ }
+
+ if (empty($cookiePairs)) {
+ return $request;
+ }
+
+ return $request->withHeader('Cookie', implode('; ', $cookiePairs));
+ }
+
+ /**
+ * Find cookies matching the scheme, host, and path
+ *
+ * @param string $scheme The http scheme to match
+ * @param string $host The host to match.
+ * @param string $path The path to match
+ * @return array An array of cookie name/value pairs
+ */
+ protected function findMatchingCookies(string $scheme, string $host, string $path): array
+ {
+ $out = [];
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ foreach ($this->cookies as $cookie) {
+ if ($scheme === 'http' && $cookie->isSecure()) {
+ continue;
+ }
+ if (strpos($path, $cookie->getPath()) !== 0) {
+ continue;
+ }
+ $domain = $cookie->getDomain();
+ $leadingDot = substr($domain, 0, 1) === '.';
+ if ($leadingDot) {
+ $domain = ltrim($domain, '.');
+ }
+
+ if ($cookie->isExpired($now)) {
+ continue;
+ }
+
+ $pattern = '/' . preg_quote($domain, '/') . '$/';
+ if (!preg_match($pattern, $host)) {
+ continue;
+ }
+
+ $out[$cookie->getName()] = $cookie->getValue();
+ }
+
+ return $out;
+ }
+
+ /**
+ * Create a new collection that includes cookies from the response.
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response Response to extract cookies from.
+ * @param \Psr\Http\Message\RequestInterface $request Request to get cookie context from.
+ * @return static
+ */
+ public function addFromResponse(ResponseInterface $response, RequestInterface $request)
+ {
+ $uri = $request->getUri();
+ $host = $uri->getHost();
+ $path = $uri->getPath() ?: '/';
+
+ $cookies = static::createFromHeader(
+ $response->getHeader('Set-Cookie'),
+ ['domain' => $host, 'path' => $path]
+ );
+ $new = clone $this;
+ foreach ($cookies as $cookie) {
+ $new->cookies[$cookie->getId()] = $cookie;
+ }
+ $new->removeExpiredCookies($host, $path);
+
+ return $new;
+ }
+
+ /**
+ * Remove expired cookies from the collection.
+ *
+ * @param string $host The host to check for expired cookies on.
+ * @param string $path The path to check for expired cookies on.
+ * @return void
+ */
+ protected function removeExpiredCookies(string $host, string $path): void
+ {
+ $time = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ $hostPattern = '/' . preg_quote($host, '/') . '$/';
+
+ foreach ($this->cookies as $i => $cookie) {
+ if (!$cookie->isExpired($time)) {
+ continue;
+ }
+ $pathMatches = strpos($path, $cookie->getPath()) === 0;
+ $hostMatches = preg_match($hostPattern, $cookie->getDomain());
+ if ($pathMatches && $hostMatches) {
+ unset($this->cookies[$i]);
+ }
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieInterface.php b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieInterface.php
new file mode 100644
index 000000000..070ff7c23
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Cookie/CookieInterface.php
@@ -0,0 +1,261 @@
+_origin = $origin;
+ $this->_isSsl = $isSsl;
+ $this->_response = $response;
+ }
+
+ /**
+ * Apply the queued headers to the response.
+ *
+ * If the builder has no Origin, or if there are no allowed domains,
+ * or if the allowed domains do not match the Origin header no headers will be applied.
+ *
+ * @return \Psr\Http\Message\MessageInterface A new instance of the response with new headers.
+ */
+ public function build(): MessageInterface
+ {
+ $response = $this->_response;
+ if (empty($this->_origin)) {
+ return $response;
+ }
+
+ if (isset($this->_headers['Access-Control-Allow-Origin'])) {
+ foreach ($this->_headers as $key => $value) {
+ $response = $response->withHeader($key, $value);
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Set the list of allowed domains.
+ *
+ * Accepts a string or an array of domains that have CORS enabled.
+ * You can use `*.example.com` wildcards to accept subdomains, or `*` to allow all domains
+ *
+ * @param string|string[] $domains The allowed domains
+ * @return $this
+ */
+ public function allowOrigin($domains)
+ {
+ $allowed = $this->_normalizeDomains((array)$domains);
+ foreach ($allowed as $domain) {
+ if (!preg_match($domain['preg'], $this->_origin)) {
+ continue;
+ }
+ $value = $domain['original'] === '*' ? '*' : $this->_origin;
+ $this->_headers['Access-Control-Allow-Origin'] = $value;
+ break;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Normalize the origin to regular expressions and put in an array format
+ *
+ * @param string[] $domains Domain names to normalize.
+ * @return array
+ */
+ protected function _normalizeDomains(array $domains): array
+ {
+ $result = [];
+ foreach ($domains as $domain) {
+ if ($domain === '*') {
+ $result[] = ['preg' => '@.@', 'original' => '*'];
+ continue;
+ }
+
+ $original = $preg = $domain;
+ if (strpos($domain, '://') === false) {
+ $preg = ($this->_isSsl ? 'https://' : 'http://') . $domain;
+ }
+ $preg = '@^' . str_replace('\*', '.*', preg_quote($preg, '@')) . '$@';
+ $result[] = compact('original', 'preg');
+ }
+
+ return $result;
+ }
+
+ /**
+ * Set the list of allowed HTTP Methods.
+ *
+ * @param string[] $methods The allowed HTTP methods
+ * @return $this
+ */
+ public function allowMethods(array $methods)
+ {
+ $this->_headers['Access-Control-Allow-Methods'] = implode(', ', $methods);
+
+ return $this;
+ }
+
+ /**
+ * Enable cookies to be sent in CORS requests.
+ *
+ * @return $this
+ */
+ public function allowCredentials()
+ {
+ $this->_headers['Access-Control-Allow-Credentials'] = 'true';
+
+ return $this;
+ }
+
+ /**
+ * Allowed headers that can be sent in CORS requests.
+ *
+ * @param string[] $headers The list of headers to accept in CORS requests.
+ * @return $this
+ */
+ public function allowHeaders(array $headers)
+ {
+ $this->_headers['Access-Control-Allow-Headers'] = implode(', ', $headers);
+
+ return $this;
+ }
+
+ /**
+ * Define the headers a client library/browser can expose to scripting
+ *
+ * @param string[] $headers The list of headers to expose CORS responses
+ * @return $this
+ */
+ public function exposeHeaders(array $headers)
+ {
+ $this->_headers['Access-Control-Expose-Headers'] = implode(', ', $headers);
+
+ return $this;
+ }
+
+ /**
+ * Define the max-age preflight OPTIONS requests are valid for.
+ *
+ * @param int|string $age The max-age for OPTIONS requests in seconds
+ * @return $this
+ */
+ public function maxAge($age)
+ {
+ $this->_headers['Access-Control-Max-Age'] = $age;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Exception/BadRequestException.php b/app/vendor/cakephp/cakephp/src/Http/Exception/BadRequestException.php
new file mode 100644
index 000000000..8277a2629
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Exception/BadRequestException.php
@@ -0,0 +1,43 @@
+headers[$header] = $value;
+ }
+
+ /**
+ * Sets HTTP response headers.
+ *
+ * @param array $headers Array of header name and value pairs.
+ * @return void
+ */
+ public function setHeaders(array $headers): void
+ {
+ $this->headers = $headers;
+ }
+
+ /**
+ * Returns array of response headers.
+ *
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Exception/InternalErrorException.php b/app/vendor/cakephp/cakephp/src/Http/Exception/InternalErrorException.php
new file mode 100644
index 000000000..efff1e06b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Exception/InternalErrorException.php
@@ -0,0 +1,38 @@
+ $value) {
+ $this->setHeader($key, (array)$value);
+ }
+ }
+
+ /**
+ * Add headers to be included in the response generated from this exception
+ *
+ * @param array $headers An array of `header => value` to append to the exception.
+ * If a header already exists, the new values will be appended to the existing ones.
+ * @return $this
+ * @deprecated 4.2.0 Use `setHeaders()` instead.
+ */
+ public function addHeaders(array $headers)
+ {
+ deprecationWarning('RedirectException::addHeaders() is deprecated, use setHeaders() instead.');
+
+ foreach ($headers as $key => $value) {
+ $this->headers[$key][] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove a header from the exception.
+ *
+ * @param string $key The header to remove.
+ * @return $this
+ * @deprecated 4.2.0 Use `setHeaders()` instead.
+ */
+ public function removeHeader(string $key)
+ {
+ deprecationWarning('RedirectException::removeHeader() is deprecated, use setHeaders() instead.');
+
+ unset($this->headers[$key]);
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Exception/ServiceUnavailableException.php b/app/vendor/cakephp/cakephp/src/Http/Exception/ServiceUnavailableException.php
new file mode 100644
index 000000000..374849b8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Exception/ServiceUnavailableException.php
@@ -0,0 +1,43 @@
+ 'flash',
+ 'element' => 'default',
+ 'plugin' => null,
+ 'params' => [],
+ 'clear' => false,
+ 'duplicate' => true,
+ ];
+
+ /**
+ * @var \Cake\Http\Session
+ */
+ protected $session;
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\Http\Session $session Session instance.
+ * @param array $config Config array.
+ * @see FlashMessage::set() For list of valid config keys.
+ */
+ public function __construct(Session $session, array $config = [])
+ {
+ $this->session = $session;
+ $this->setConfig($config);
+ }
+
+ /**
+ * Store flash messages that can be output in the view.
+ *
+ * If you make consecutive calls to this method, the messages will stack
+ * (if they are set with the same flash key)
+ *
+ * ### Options:
+ *
+ * - `key` The key to set under the session's Flash key.
+ * - `element` The element used to render the flash message. You can use
+ * `'SomePlugin.name'` style value for flash elements from a plugin.
+ * - `plugin` Plugin name to use element from.
+ * - `params` An array of variables to be made available to the element.
+ * - `clear` A bool stating if the current stack should be cleared to start a new one.
+ * - `escape` Set to false to allow templates to print out HTML content.
+ *
+ * @param string $message Message to be flashed.
+ * @param array $options An array of options
+ * @return void
+ * @see FlashMessage::$_defaultConfig For default values for the options.
+ */
+ public function set($message, array $options = []): void
+ {
+ $options += (array)$this->getConfig();
+
+ if (isset($options['escape']) && !isset($options['params']['escape'])) {
+ $options['params']['escape'] = $options['escape'];
+ }
+
+ [$plugin, $element] = pluginSplit($options['element']);
+ if ($options['plugin']) {
+ $plugin = $options['plugin'];
+ }
+
+ if ($plugin) {
+ $options['element'] = $plugin . '.flash/' . $element;
+ } else {
+ $options['element'] = 'flash/' . $element;
+ }
+
+ $messages = [];
+ if (!$options['clear']) {
+ $messages = (array)$this->session->read('Flash.' . $options['key']);
+ }
+
+ if (!$options['duplicate']) {
+ foreach ($messages as $existingMessage) {
+ if ($existingMessage['message'] === $message) {
+ return;
+ }
+ }
+ }
+
+ $messages[] = [
+ 'message' => $message,
+ 'key' => $options['key'],
+ 'element' => $options['element'],
+ 'params' => $options['params'],
+ ];
+
+ $this->session->write('Flash.' . $options['key'], $messages);
+ }
+
+ /**
+ * Set an exception's message as flash message.
+ *
+ * The following options will be set by default if unset:
+ * ```
+ * 'element' => 'error',
+ * `params' => ['code' => $exception->getCode()]
+ * ```
+ *
+ * @param \Throwable $exception Exception instance.
+ * @param array $options An array of options.
+ * @return void
+ * @see FlashMessage::set() For list of valid options
+ */
+ public function setExceptionMessage(Throwable $exception, array $options = []): void
+ {
+ if (!isset($options['element'])) {
+ $options['element'] = 'error';
+ }
+ if (!isset($options['params']['code'])) {
+ $options['params']['code'] = $exception->getCode();
+ }
+
+ $message = $exception->getMessage();
+ $this->set($message, $options);
+ }
+
+ /**
+ * Get the messages for given key and remove from session.
+ *
+ * @param string $key The key for get messages for.
+ * @return array|null
+ */
+ public function consume(string $key): ?array
+ {
+ return $this->session->consume("Flash.{$key}");
+ }
+
+ /**
+ * Set a success message.
+ *
+ * The `'element'` option will be set to `'success'`.
+ *
+ * @param string $message Message to flash.
+ * @param array $options An array of options.
+ * @return void
+ * @see FlashMessage::set() For list of valid options
+ */
+ public function success(string $message, array $options = []): void
+ {
+ $options['element'] = 'success';
+ $this->set($message, $options);
+ }
+
+ /**
+ * Set an success message.
+ *
+ * The `'element'` option will be set to `'error'`.
+ *
+ * @param string $message Message to flash.
+ * @param array $options An array of options.
+ * @return void
+ * @see FlashMessage::set() For list of valid options
+ */
+ public function error(string $message, array $options = []): void
+ {
+ $options['element'] = 'error';
+ $this->set($message, $options);
+ }
+
+ /**
+ * Set a warning message.
+ *
+ * The `'element'` option will be set to `'warning'`.
+ *
+ * @param string $message Message to flash.
+ * @param array $options An array of options.
+ * @return void
+ * @see FlashMessage::set() For list of valid options
+ */
+ public function warning(string $message, array $options = []): void
+ {
+ $options['element'] = 'warning';
+ $this->set($message, $options);
+ }
+
+ /**
+ * Set an info message.
+ *
+ * The `'element'` option will be set to `'info'`.
+ *
+ * @param string $message Message to flash.
+ * @param array $options An array of options.
+ * @return void
+ * @see FlashMessage::set() For list of valid options
+ */
+ public function info(string $message, array $options = []): void
+ {
+ $options['element'] = 'info';
+ $this->set($message, $options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Http/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/BodyParserMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/BodyParserMiddleware.php
new file mode 100644
index 000000000..80dc98423
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/BodyParserMiddleware.php
@@ -0,0 +1,216 @@
+ true, 'xml' => false, 'methods' => null];
+ if ($options['json']) {
+ $this->addParser(
+ ['application/json', 'text/json'],
+ Closure::fromCallable([$this, 'decodeJson'])
+ );
+ }
+ if ($options['xml']) {
+ $this->addParser(
+ ['application/xml', 'text/xml'],
+ Closure::fromCallable([$this, 'decodeXml'])
+ );
+ }
+ if ($options['methods']) {
+ $this->setMethods($options['methods']);
+ }
+ }
+
+ /**
+ * Set the HTTP methods to parse request bodies on.
+ *
+ * @param string[] $methods The methods to parse data on.
+ * @return $this
+ */
+ public function setMethods(array $methods)
+ {
+ $this->methods = $methods;
+
+ return $this;
+ }
+
+ /**
+ * Get the HTTP methods to parse request bodies on.
+ *
+ * @return string[]
+ */
+ public function getMethods(): array
+ {
+ return $this->methods;
+ }
+
+ /**
+ * Add a parser.
+ *
+ * Map a set of content-type header values to be parsed by the $parser.
+ *
+ * ### Example
+ *
+ * An naive CSV request body parser could be built like so:
+ *
+ * ```
+ * $parser->addParser(['text/csv'], function ($body) {
+ * return str_getcsv($body);
+ * });
+ * ```
+ *
+ * @param string[] $types An array of content-type header values to match. eg. application/json
+ * @param \Closure $parser The parser function. Must return an array of data to be inserted
+ * into the request.
+ * @return $this
+ */
+ public function addParser(array $types, Closure $parser)
+ {
+ foreach ($types as $type) {
+ $type = strtolower($type);
+ $this->parsers[$type] = $parser;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the current parsers
+ *
+ * @return \Closure[]
+ */
+ public function getParsers(): array
+ {
+ return $this->parsers;
+ }
+
+ /**
+ * Apply the middleware.
+ *
+ * Will modify the request adding a parsed body if the content-type is known.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ if (!in_array($request->getMethod(), $this->methods, true)) {
+ return $handler->handle($request);
+ }
+ [$type] = explode(';', $request->getHeaderLine('Content-Type'));
+ $type = strtolower($type);
+ if (!isset($this->parsers[$type])) {
+ return $handler->handle($request);
+ }
+
+ $parser = $this->parsers[$type];
+ $result = $parser($request->getBody()->getContents());
+ if (!is_array($result)) {
+ throw new BadRequestException();
+ }
+ $request = $request->withParsedBody($result);
+
+ return $handler->handle($request);
+ }
+
+ /**
+ * Decode JSON into an array.
+ *
+ * @param string $body The request body to decode
+ * @return array|null
+ */
+ protected function decodeJson(string $body)
+ {
+ if ($body === '') {
+ return [];
+ }
+ $decoded = json_decode($body, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ return (array)$decoded;
+ }
+
+ return null;
+ }
+
+ /**
+ * Decode XML into an array.
+ *
+ * @param string $body The request body to decode
+ * @return array
+ */
+ protected function decodeXml(string $body): array
+ {
+ try {
+ $xml = Xml::build($body, ['return' => 'domdocument', 'readFile' => false]);
+ // We might not get child nodes if there are nested inline entities.
+ if ((int)$xml->childNodes->length > 0) {
+ return Xml::toArray($xml);
+ }
+
+ return [];
+ } catch (XmlException $e) {
+ return [];
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/ClosureDecoratorMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/ClosureDecoratorMiddleware.php
new file mode 100644
index 000000000..c85640716
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/ClosureDecoratorMiddleware.php
@@ -0,0 +1,81 @@
+callable = $callable;
+ }
+
+ /**
+ * Run the callable to process an incoming server request.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler Request handler instance.
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ return ($this->callable)(
+ $request,
+ $handler
+ );
+ }
+
+ /**
+ * @internal
+ * @return callable
+ */
+ public function getCallable(): callable
+ {
+ return $this->callable;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/CspMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/CspMiddleware.php
new file mode 100644
index 000000000..f880bc02c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/CspMiddleware.php
@@ -0,0 +1,72 @@
+csp = $csp;
+ }
+
+ /**
+ * Serve assets if the path matches one.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $response = $handler->handle($request);
+
+ /** @var \Psr\Http\Message\ResponseInterface */
+ return $this->csp->injectCSPHeader($response);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php
new file mode 100644
index 000000000..44e8cf0e8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php
@@ -0,0 +1,437 @@
+Form->create(...)` is used in a view.
+ *
+ * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
+ */
+class CsrfProtectionMiddleware implements MiddlewareInterface
+{
+ /**
+ * Config for the CSRF handling.
+ *
+ * - `cookieName` The name of the cookie to send.
+ * - `expiry` A strotime compatible value of how long the CSRF token should last.
+ * Defaults to browser session.
+ * - `secure` Whether or not the cookie will be set with the Secure flag. Defaults to false.
+ * - `httponly` Whether or not the cookie will be set with the HttpOnly flag. Defaults to false.
+ * - `samesite` "SameSite" attribute for cookies. Defaults to `null`.
+ * Valid values: `CookieInterface::SAMESITE_LAX`, `CookieInterface::SAMESITE_STRICT`,
+ * `CookieInterface::SAMESITE_NONE` or `null`.
+ * - `field` The form field to check. Changing this will also require configuring
+ * FormHelper.
+ *
+ * @var array
+ */
+ protected $_config = [
+ 'cookieName' => 'csrfToken',
+ 'expiry' => 0,
+ 'secure' => false,
+ 'httponly' => false,
+ 'samesite' => null,
+ 'field' => '_csrfToken',
+ ];
+
+ /**
+ * Callback for deciding whether or not to skip the token check for particular request.
+ *
+ * CSRF protection token check will be skipped if the callback returns `true`.
+ *
+ * @var callable|null
+ */
+ protected $skipCheckCallback;
+
+ /**
+ * @var int
+ */
+ public const TOKEN_VALUE_LENGTH = 16;
+
+ /**
+ * Tokens have an hmac generated so we can ensure
+ * that tokens were generated by our application.
+ *
+ * Should be TOKEN_VALUE_LENGTH + strlen(hmac)
+ *
+ * We are currently using sha1 for the hmac which
+ * creates 40 bytes.
+ *
+ * @var int
+ */
+ public const TOKEN_WITH_CHECKSUM_LENGTH = 56;
+
+ /**
+ * Constructor
+ *
+ * @param array $config Config options. See $_config for valid keys.
+ */
+ public function __construct(array $config = [])
+ {
+ if (array_key_exists('httpOnly', $config)) {
+ $config['httponly'] = $config['httpOnly'];
+ deprecationWarning('Option `httpOnly` is deprecated. Use lowercased `httponly` instead.');
+ }
+
+ $this->_config = $config + $this->_config;
+ }
+
+ /**
+ * Checks and sets the CSRF token depending on the HTTP verb.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $hasData = in_array($method, ['PUT', 'POST', 'DELETE', 'PATCH'], true)
+ || $request->getParsedBody();
+
+ if (
+ $hasData
+ && $this->skipCheckCallback !== null
+ && call_user_func($this->skipCheckCallback, $request) === true
+ ) {
+ $request = $this->_unsetTokenField($request);
+
+ return $handler->handle($request);
+ }
+ if ($request->getAttribute('csrfToken')) {
+ throw new RuntimeException(
+ 'A CSRF token is already set in the request.' .
+ "\n" .
+ 'Ensure you do not have the CSRF middleware applied more than once. ' .
+ 'Check both your `Application::middleware()` method and `config/routes.php`.'
+ );
+ }
+
+ $cookies = $request->getCookieParams();
+ $cookieData = Hash::get($cookies, $this->_config['cookieName']);
+
+ if (is_string($cookieData) && strlen($cookieData) > 0) {
+ $request = $request->withAttribute('csrfToken', $this->saltToken($cookieData));
+ }
+
+ if ($method === 'GET' && $cookieData === null) {
+ $token = $this->createToken();
+ $request = $request->withAttribute('csrfToken', $this->saltToken($token));
+ /** @var mixed $response */
+ $response = $handler->handle($request);
+
+ return $this->_addTokenCookie($token, $request, $response);
+ }
+
+ if ($hasData) {
+ $this->_validateToken($request);
+ $request = $this->_unsetTokenField($request);
+ }
+
+ return $handler->handle($request);
+ }
+
+ /**
+ * Set callback for allowing to skip token check for particular request.
+ *
+ * The callback will receive request instance as argument and must return
+ * `true` if you want to skip token check for the current request.
+ *
+ * @deprecated 4.1.0 Use skipCheckCallback instead.
+ * @param callable $callback A callable.
+ * @return $this
+ */
+ public function whitelistCallback(callable $callback)
+ {
+ deprecationWarning('`whitelistCallback()` is deprecated. Use `skipCheckCallback()` instead.');
+ $this->skipCheckCallback = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Set callback for allowing to skip token check for particular request.
+ *
+ * The callback will receive request instance as argument and must return
+ * `true` if you want to skip token check for the current request.
+ *
+ * @param callable $callback A callable.
+ * @return $this
+ */
+ public function skipCheckCallback(callable $callback)
+ {
+ $this->skipCheckCallback = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Remove CSRF protection token from request data.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request object.
+ * @return \Psr\Http\Message\ServerRequestInterface
+ */
+ protected function _unsetTokenField(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $body = $request->getParsedBody();
+ if (is_array($body)) {
+ unset($body[$this->_config['field']]);
+ $request = $request->withParsedBody($body);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Create a new token to be used for CSRF protection
+ *
+ * @return string
+ * @deprecated 4.0.6 Use {@link createToken()} instead.
+ */
+ protected function _createToken(): string
+ {
+ deprecationWarning('_createToken() is deprecated. Use createToken() instead.');
+
+ return $this->createToken();
+ }
+
+ /**
+ * Test if the token predates salted tokens.
+ *
+ * These tokens are hexadecimal values and equal
+ * to the token with checksum length. While they are vulnerable
+ * to BREACH they should rotate over time and support will be dropped
+ * in 5.x.
+ *
+ * @param string $token The token to test.
+ * @return bool
+ */
+ protected function isHexadecimalToken(string $token): bool
+ {
+ return preg_match('/^[a-f0-9]{' . static::TOKEN_WITH_CHECKSUM_LENGTH . '}$/', $token) === 1;
+ }
+
+ /**
+ * Create a new token to be used for CSRF protection
+ *
+ * @return string
+ */
+ public function createToken(): string
+ {
+ $value = Security::randomBytes(static::TOKEN_VALUE_LENGTH);
+
+ return base64_encode($value . hash_hmac('sha1', $value, Security::getSalt()));
+ }
+
+ /**
+ * Apply entropy to a CSRF token
+ *
+ * To avoid BREACH apply a random salt value to a token
+ * When the token is compared to the session the token needs
+ * to be unsalted.
+ *
+ * @param string $token The token to salt.
+ * @return string The salted token with the salt appended.
+ */
+ public function saltToken(string $token): string
+ {
+ if ($this->isHexadecimalToken($token)) {
+ return $token;
+ }
+ $decoded = base64_decode($token, true);
+ $length = strlen($decoded);
+ $salt = Security::randomBytes($length);
+ $salted = '';
+ for ($i = 0; $i < $length; $i++) {
+ // XOR the token and salt together so that we can reverse it later.
+ $salted .= chr(ord($decoded[$i]) ^ ord($salt[$i]));
+ }
+
+ return base64_encode($salted . $salt);
+ }
+
+ /**
+ * Remove the salt from a CSRF token.
+ *
+ * If the token is not TOKEN_VALUE_LENGTH * 2 it is an old
+ * unsalted value that is supported for backwards compatibility.
+ *
+ * @param string $token The token that could be salty.
+ * @return string An unsalted token.
+ */
+ public function unsaltToken(string $token): string
+ {
+ if ($this->isHexadecimalToken($token)) {
+ return $token;
+ }
+ $decoded = base64_decode($token, true);
+ if ($decoded === false || strlen($decoded) !== static::TOKEN_WITH_CHECKSUM_LENGTH * 2) {
+ return $token;
+ }
+ $salted = substr($decoded, 0, static::TOKEN_WITH_CHECKSUM_LENGTH);
+ $salt = substr($decoded, static::TOKEN_WITH_CHECKSUM_LENGTH);
+
+ $unsalted = '';
+ for ($i = 0; $i < static::TOKEN_WITH_CHECKSUM_LENGTH; $i++) {
+ // Reverse the XOR to desalt.
+ $unsalted .= chr(ord($salted[$i]) ^ ord($salt[$i]));
+ }
+
+ return base64_encode($unsalted);
+ }
+
+ /**
+ * Verify that CSRF token was originally generated by the receiving application.
+ *
+ * @param string $token The CSRF token.
+ * @return bool
+ */
+ protected function _verifyToken(string $token): bool
+ {
+ // If we have a hexadecimal value we're in a compatibility mode from before
+ // tokens were salted on each request.
+ if ($this->isHexadecimalToken($token)) {
+ $decoded = $token;
+ } else {
+ $decoded = base64_decode($token, true);
+ }
+ if (strlen($decoded) <= static::TOKEN_VALUE_LENGTH) {
+ return false;
+ }
+
+ $key = substr($decoded, 0, static::TOKEN_VALUE_LENGTH);
+ $hmac = substr($decoded, static::TOKEN_VALUE_LENGTH);
+
+ $expectedHmac = hash_hmac('sha1', $key, Security::getSalt());
+
+ return hash_equals($hmac, $expectedHmac);
+ }
+
+ /**
+ * Add a CSRF token to the response cookies.
+ *
+ * @param string $token The token to add.
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to validate against.
+ * @param \Psr\Http\Message\ResponseInterface $response The response.
+ * @return \Psr\Http\Message\ResponseInterface $response Modified response.
+ */
+ protected function _addTokenCookie(
+ string $token,
+ ServerRequestInterface $request,
+ ResponseInterface $response
+ ): ResponseInterface {
+ $cookie = $this->_createCookie($token, $request);
+ if ($response instanceof Response) {
+ return $response->withCookie($cookie);
+ }
+
+ return $response->withAddedHeader('Set-Cookie', $cookie->toHeaderValue());
+ }
+
+ /**
+ * Validate the request data against the cookie token.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to validate against.
+ * @return void
+ * @throws \Cake\Http\Exception\InvalidCsrfTokenException When the CSRF token is invalid or missing.
+ */
+ protected function _validateToken(ServerRequestInterface $request): void
+ {
+ $cookie = Hash::get($request->getCookieParams(), $this->_config['cookieName']);
+
+ if (!$cookie || !is_string($cookie)) {
+ throw new InvalidCsrfTokenException(__d('cake', 'Missing or incorrect CSRF cookie type.'));
+ }
+
+ if (!$this->_verifyToken($cookie)) {
+ $exception = new InvalidCsrfTokenException(__d('cake', 'Missing or invalid CSRF cookie.'));
+
+ $expiredCookie = $this->_createCookie('', $request)->withExpired();
+ $exception->setHeader('Set-Cookie', $expiredCookie->toHeaderValue());
+
+ throw $exception;
+ }
+
+ $body = $request->getParsedBody();
+ if (is_array($body) || $body instanceof ArrayAccess) {
+ $post = (string)Hash::get($body, $this->_config['field']);
+ $post = $this->unsaltToken($post);
+ if (hash_equals($post, $cookie)) {
+ return;
+ }
+ }
+
+ $header = $request->getHeaderLine('X-CSRF-Token');
+ $header = $this->unsaltToken($header);
+ if (hash_equals($header, $cookie)) {
+ return;
+ }
+
+ throw new InvalidCsrfTokenException(__d(
+ 'cake',
+ 'CSRF token from either the request body or request headers did not match or is missing.'
+ ));
+ }
+
+ /**
+ * Create response cookie
+ *
+ * @param string $value Cookie value
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request object.
+ * @return \Cake\Http\Cookie\CookieInterface
+ */
+ protected function _createCookie(string $value, ServerRequestInterface $request): CookieInterface
+ {
+ $cookie = Cookie::create(
+ $this->_config['cookieName'],
+ $value,
+ [
+ 'expires' => $this->_config['expiry'] ?: null,
+ 'path' => $request->getAttribute('webroot'),
+ 'secure' => $this->_config['secure'],
+ 'httponly' => $this->_config['httponly'],
+ 'samesite' => $this->_config['samesite'],
+ ]
+ );
+
+ return $cookie;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/DoublePassDecoratorMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/DoublePassDecoratorMiddleware.php
new file mode 100644
index 000000000..287d80c59
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/DoublePassDecoratorMiddleware.php
@@ -0,0 +1,87 @@
+callable = $callable;
+ }
+
+ /**
+ * Run the internal double pass callable to process an incoming server request.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler Request handler instance.
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ return ($this->callable)(
+ $request,
+ new Response(),
+ function ($request, $res) use ($handler) {
+ return $handler->handle($request);
+ }
+ );
+ }
+
+ /**
+ * @internal
+ * @return callable
+ */
+ public function getCallable(): callable
+ {
+ return $this->callable;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/EncryptedCookieMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/EncryptedCookieMiddleware.php
new file mode 100644
index 000000000..4786cb589
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/EncryptedCookieMiddleware.php
@@ -0,0 +1,175 @@
+cookieNames = $cookieNames;
+ $this->key = $key;
+ $this->cipherType = $cipherType;
+ }
+
+ /**
+ * Apply cookie encryption/decryption.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ if ($request->getCookieParams()) {
+ $request = $this->decodeCookies($request);
+ }
+
+ $response = $handler->handle($request);
+ if ($response->hasHeader('Set-Cookie')) {
+ $response = $this->encodeSetCookieHeader($response);
+ }
+ if ($response instanceof Response) {
+ $response = $this->encodeCookies($response);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Fetch the cookie encryption key.
+ *
+ * Part of the CookieCryptTrait implementation.
+ *
+ * @return string
+ */
+ protected function _getCookieEncryptionKey(): string
+ {
+ return $this->key;
+ }
+
+ /**
+ * Decode cookies from the request.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to decode cookies from.
+ * @return \Psr\Http\Message\ServerRequestInterface Updated request with decoded cookies.
+ */
+ protected function decodeCookies(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $cookies = $request->getCookieParams();
+ foreach ($this->cookieNames as $name) {
+ if (isset($cookies[$name])) {
+ $cookies[$name] = $this->_decrypt($cookies[$name], $this->cipherType, $this->key);
+ }
+ }
+
+ return $request->withCookieParams($cookies);
+ }
+
+ /**
+ * Encode cookies from a response's CookieCollection.
+ *
+ * @param \Cake\Http\Response $response The response to encode cookies in.
+ * @return \Cake\Http\Response Updated response with encoded cookies.
+ */
+ protected function encodeCookies(Response $response): Response
+ {
+ /** @var \Cake\Http\Cookie\CookieInterface[] $cookies */
+ $cookies = $response->getCookieCollection();
+ foreach ($cookies as $cookie) {
+ if (in_array($cookie->getName(), $this->cookieNames, true)) {
+ $value = $this->_encrypt($cookie->getValue(), $this->cipherType);
+ $response = $response->withCookie($cookie->withValue($value));
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Encode cookies from a response's Set-Cookie header
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response The response to encode cookies in.
+ * @return \Psr\Http\Message\ResponseInterface Updated response with encoded cookies.
+ */
+ protected function encodeSetCookieHeader(ResponseInterface $response): ResponseInterface
+ {
+ /** @var \Cake\Http\Cookie\CookieInterface[] $cookies */
+ $cookies = CookieCollection::createFromHeader($response->getHeader('Set-Cookie'));
+ $header = [];
+ foreach ($cookies as $cookie) {
+ if (in_array($cookie->getName(), $this->cookieNames, true)) {
+ $value = $this->_encrypt($cookie->getValue(), $this->cipherType);
+ $cookie = $cookie->withValue($value);
+ }
+ $header[] = $cookie->toHeaderValue();
+ }
+
+ return $response->withHeader('Set-Cookie', $header);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/HttpsEnforcerMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/HttpsEnforcerMiddleware.php
new file mode 100644
index 000000000..491417b26
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/HttpsEnforcerMiddleware.php
@@ -0,0 +1,98 @@
+ true,
+ 'statusCode' => 301,
+ 'headers' => [],
+ 'disableOnDebug' => true,
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param array $config The options to use.
+ * @see self::$config
+ */
+ public function __construct(array $config = [])
+ {
+ $this->config = $config + $this->config;
+ }
+
+ /**
+ * Check whether request has been made using HTTPS.
+ *
+ * Depending on the configuration and request method, either redirects to
+ * same URL with https or throws an exception.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ * @throws \Cake\Http\Exception\BadRequestException
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ if (
+ $request->getUri()->getScheme() === 'https'
+ || ($this->config['disableOnDebug']
+ && Configure::read('debug'))
+ ) {
+ return $handler->handle($request);
+ }
+
+ if ($this->config['redirect'] && $request->getMethod() === 'GET') {
+ $uri = $request->getUri()->withScheme('https');
+
+ return new RedirectResponse(
+ $uri,
+ $this->config['statusCode'],
+ $this->config['headers']
+ );
+ }
+
+ throw new BadRequestException(
+ 'Requests to this URL must be made with HTTPS.'
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/SecurityHeadersMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/SecurityHeadersMiddleware.php
new file mode 100644
index 000000000..b458522fa
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/SecurityHeadersMiddleware.php
@@ -0,0 +1,264 @@
+headers['x-content-type-options'] = self::NOSNIFF;
+
+ return $this;
+ }
+
+ /**
+ * X-Download-Options
+ *
+ * Sets the header value for it to 'noopen'
+ *
+ * @link https://msdn.microsoft.com/en-us/library/jj542450(v=vs.85).aspx
+ * @return $this
+ */
+ public function noOpen()
+ {
+ $this->headers['x-download-options'] = self::NOOPEN;
+
+ return $this;
+ }
+
+ /**
+ * Referrer-Policy
+ *
+ * @link https://w3c.github.io/webappsec-referrer-policy
+ * @param string $policy Policy value. Available Value: 'no-referrer', 'no-referrer-when-downgrade', 'origin',
+ * 'origin-when-cross-origin', 'same-origin', 'strict-origin', 'strict-origin-when-cross-origin', 'unsafe-url'
+ * @return $this
+ */
+ public function setReferrerPolicy(string $policy = self::SAME_ORIGIN)
+ {
+ $available = [
+ self::NO_REFERRER,
+ self::NO_REFERRER_WHEN_DOWNGRADE,
+ self::ORIGIN,
+ self::ORIGIN_WHEN_CROSS_ORIGIN,
+ self::SAME_ORIGIN,
+ self::STRICT_ORIGIN,
+ self::STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
+ self::UNSAFE_URL,
+ ];
+
+ $this->checkValues($policy, $available);
+ $this->headers['referrer-policy'] = $policy;
+
+ return $this;
+ }
+
+ /**
+ * X-Frame-Options
+ *
+ * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ * @param string $option Option value. Available Values: 'deny', 'sameorigin', 'allow-from '
+ * @param string $url URL if mode is `allow-from`
+ * @return $this
+ */
+ public function setXFrameOptions(string $option = self::SAMEORIGIN, ?string $url = null)
+ {
+ $this->checkValues($option, [self::DENY, self::SAMEORIGIN, self::ALLOW_FROM]);
+
+ if ($option === self::ALLOW_FROM) {
+ if (empty($url)) {
+ throw new InvalidArgumentException('The 2nd arg $url can not be empty when `allow-from` is used');
+ }
+ $option .= ' ' . $url;
+ }
+
+ $this->headers['x-frame-options'] = $option;
+
+ return $this;
+ }
+
+ /**
+ * X-XSS-Protection
+ *
+ * @link https://blogs.msdn.microsoft.com/ieinternals/2011/01/31/controlling-the-xss-filter
+ * @param string $mode Mode value. Available Values: '1', '0', 'block'
+ * @return $this
+ */
+ public function setXssProtection(string $mode = self::XSS_BLOCK)
+ {
+ $mode = $mode;
+
+ if ($mode === self::XSS_BLOCK) {
+ $mode = self::XSS_ENABLED_BLOCK;
+ }
+
+ $this->checkValues($mode, [self::XSS_ENABLED, self::XSS_DISABLED, self::XSS_ENABLED_BLOCK]);
+ $this->headers['x-xss-protection'] = $mode;
+
+ return $this;
+ }
+
+ /**
+ * X-Permitted-Cross-Domain-Policies
+ *
+ * @link https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html
+ * @param string $policy Policy value. Available Values: 'all', 'none', 'master-only', 'by-content-type',
+ * 'by-ftp-filename'
+ * @return $this
+ */
+ public function setCrossDomainPolicy(string $policy = self::ALL)
+ {
+ $this->checkValues($policy, [
+ self::ALL,
+ self::NONE,
+ self::MASTER_ONLY,
+ self::BY_CONTENT_TYPE,
+ self::BY_FTP_FILENAME,
+ ]);
+ $this->headers['x-permitted-cross-domain-policies'] = $policy;
+
+ return $this;
+ }
+
+ /**
+ * Convenience method to check if a value is in the list of allowed args
+ *
+ * @throws \InvalidArgumentException Thrown when a value is invalid.
+ * @param string $value Value to check
+ * @param string[] $allowed List of allowed values
+ * @return void
+ */
+ protected function checkValues(string $value, array $allowed): void
+ {
+ if (!in_array($value, $allowed, true)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Invalid arg `%s`, use one of these: %s',
+ $value,
+ implode(', ', $allowed)
+ ));
+ }
+ }
+
+ /**
+ * Serve assets if the path matches one.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $response = $handler->handle($request);
+ foreach ($this->headers as $header => $value) {
+ $response = $response->withHeader($header, $value);
+ }
+
+ return $response;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Middleware/SessionCsrfProtectionMiddleware.php b/app/vendor/cakephp/cakephp/src/Http/Middleware/SessionCsrfProtectionMiddleware.php
new file mode 100644
index 000000000..4b3beae79
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Middleware/SessionCsrfProtectionMiddleware.php
@@ -0,0 +1,270 @@
+Form->create(...)` is used in a view.
+ *
+ * If you use this middleware *do not* also use CsrfProtectionMiddleware.
+ *
+ * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#sychronizer-token-pattern
+ */
+class SessionCsrfProtectionMiddleware implements MiddlewareInterface
+{
+ /**
+ * Config for the CSRF handling.
+ *
+ * - `key` The session key to use. Defaults to `csrfToken`
+ * - `field` The form field to check. Changing this will also require configuring
+ * FormHelper.
+ *
+ * @var array
+ */
+ protected $_config = [
+ 'key' => 'csrfToken',
+ 'field' => '_csrfToken',
+ ];
+
+ /**
+ * Callback for deciding whether or not to skip the token check for particular request.
+ *
+ * CSRF protection token check will be skipped if the callback returns `true`.
+ *
+ * @var callable|null
+ */
+ protected $skipCheckCallback;
+
+ /**
+ * @var int
+ */
+ public const TOKEN_VALUE_LENGTH = 32;
+
+ /**
+ * Constructor
+ *
+ * @param array $config Config options. See $_config for valid keys.
+ */
+ public function __construct(array $config = [])
+ {
+ $this->_config = $config + $this->_config;
+ }
+
+ /**
+ * Checks and sets the CSRF token depending on the HTTP verb.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $method = $request->getMethod();
+ $hasData = in_array($method, ['PUT', 'POST', 'DELETE', 'PATCH'], true)
+ || $request->getParsedBody();
+
+ if (
+ $hasData
+ && $this->skipCheckCallback !== null
+ && call_user_func($this->skipCheckCallback, $request) === true
+ ) {
+ $request = $this->unsetTokenField($request);
+
+ return $handler->handle($request);
+ }
+
+ $session = $request->getAttribute('session');
+ if (!$session || !($session instanceof Session)) {
+ throw new RuntimeException('You must have a `session` attribute to use session based CSRF tokens');
+ }
+
+ $token = $session->read($this->_config['key']);
+ if ($token === null) {
+ $token = $this->createToken();
+ $session->write($this->_config['key'], $token);
+ }
+ $request = $request->withAttribute('csrfToken', $this->saltToken($token));
+
+ if ($method === 'GET') {
+ return $handler->handle($request);
+ }
+
+ if ($hasData) {
+ $this->validateToken($request, $session);
+ $request = $this->unsetTokenField($request);
+ }
+
+ return $handler->handle($request);
+ }
+
+ /**
+ * Set callback for allowing to skip token check for particular request.
+ *
+ * The callback will receive request instance as argument and must return
+ * `true` if you want to skip token check for the current request.
+ *
+ * @param callable $callback A callable.
+ * @return $this
+ */
+ public function skipCheckCallback(callable $callback)
+ {
+ $this->skipCheckCallback = $callback;
+
+ return $this;
+ }
+
+ /**
+ * Apply entropy to a CSRF token
+ *
+ * To avoid BREACH apply a random salt value to a token
+ * When the token is compared to the session the token needs
+ * to be unsalted.
+ *
+ * @param string $token The token to salt.
+ * @return string The salted token with the salt appended.
+ */
+ public function saltToken(string $token): string
+ {
+ $decoded = base64_decode($token);
+ $length = strlen($decoded);
+ $salt = Security::randomBytes($length);
+ $salted = '';
+ for ($i = 0; $i < $length; $i++) {
+ // XOR the token and salt together so that we can reverse it later.
+ $salted .= chr(ord($decoded[$i]) ^ ord($salt[$i]));
+ }
+
+ return base64_encode($salted . $salt);
+ }
+
+ /**
+ * Remove the salt from a CSRF token.
+ *
+ * If the token is not TOKEN_VALUE_LENGTH * 2 it is an old
+ * unsalted value that is supported for backwards compatibility.
+ *
+ * @param string $token The token that could be salty.
+ * @return string An unsalted token.
+ */
+ protected function unsaltToken(string $token): string
+ {
+ $decoded = base64_decode($token, true);
+ if ($decoded === false || strlen($decoded) !== static::TOKEN_VALUE_LENGTH * 2) {
+ return $token;
+ }
+ $salted = substr($decoded, 0, static::TOKEN_VALUE_LENGTH);
+ $salt = substr($decoded, static::TOKEN_VALUE_LENGTH);
+
+ $unsalted = '';
+ for ($i = 0; $i < static::TOKEN_VALUE_LENGTH; $i++) {
+ // Reverse the XOR to desalt.
+ $unsalted .= chr(ord($salted[$i]) ^ ord($salt[$i]));
+ }
+
+ return base64_encode($unsalted);
+ }
+
+ /**
+ * Remove CSRF protection token from request data.
+ *
+ * This ensures that the token does not cause failures during
+ * form tampering protection.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request object.
+ * @return \Psr\Http\Message\ServerRequestInterface
+ */
+ protected function unsetTokenField(ServerRequestInterface $request): ServerRequestInterface
+ {
+ $body = $request->getParsedBody();
+ if (is_array($body)) {
+ unset($body[$this->_config['field']]);
+ $request = $request->withParsedBody($body);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Create a new token to be used for CSRF protection
+ *
+ * This token is a simple unique random value as the compare
+ * value is stored in the session where it cannot be tampered with.
+ *
+ * @return string
+ */
+ public function createToken(): string
+ {
+ return base64_encode(Security::randomBytes(static::TOKEN_VALUE_LENGTH));
+ }
+
+ /**
+ * Validate the request data against the cookie token.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to validate against.
+ * @param \Cake\Http\Session $session The session instance.
+ * @return void
+ * @throws \Cake\Http\Exception\InvalidCsrfTokenException When the CSRF token is invalid or missing.
+ */
+ protected function validateToken(ServerRequestInterface $request, Session $session): void
+ {
+ $token = $session->read($this->_config['key']);
+ if (!$token || !is_string($token)) {
+ throw new InvalidCsrfTokenException(__d('cake', 'Missing or incorrect CSRF session key'));
+ }
+
+ $body = $request->getParsedBody();
+ if (is_array($body) || $body instanceof ArrayAccess) {
+ $post = (string)Hash::get($body, $this->_config['field']);
+ $post = $this->unsaltToken($post);
+ if (hash_equals($post, $token)) {
+ return;
+ }
+ }
+
+ $header = $request->getHeaderLine('X-CSRF-Token');
+ $header = $this->unsaltToken($header);
+ if (hash_equals($header, $token)) {
+ return;
+ }
+
+ throw new InvalidCsrfTokenException(__d(
+ 'cake',
+ 'CSRF token from either the request body or request headers did not match or is missing.'
+ ));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/MiddlewareApplication.php b/app/vendor/cakephp/cakephp/src/Http/MiddlewareApplication.php
new file mode 100644
index 000000000..89cf261a5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/MiddlewareApplication.php
@@ -0,0 +1,56 @@
+ 'Not found', 'status' => 404]);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/MiddlewareQueue.php b/app/vendor/cakephp/cakephp/src/Http/MiddlewareQueue.php
new file mode 100644
index 000000000..ee5704183
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/MiddlewareQueue.php
@@ -0,0 +1,322 @@
+
+ */
+class MiddlewareQueue implements Countable, SeekableIterator
+{
+ /**
+ * Internal position for iterator.
+ *
+ * @var int
+ */
+ protected $position = 0;
+
+ /**
+ * The queue of middlewares.
+ *
+ * @var array
+ * @psalm-var array
+ */
+ protected $queue = [];
+
+ /**
+ * Constructor
+ *
+ * @param array $middleware The list of middleware to append.
+ */
+ public function __construct(array $middleware = [])
+ {
+ $this->queue = $middleware;
+ }
+
+ /**
+ * Resolve middleware name to a PSR 15 compliant middleware instance.
+ *
+ * @param string|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware to resolve.
+ * @return \Psr\Http\Server\MiddlewareInterface
+ * @throws \RuntimeException If Middleware not found.
+ */
+ protected function resolve($middleware): MiddlewareInterface
+ {
+ if (is_string($middleware)) {
+ $className = App::className($middleware, 'Middleware', 'Middleware');
+ if ($className === null) {
+ throw new RuntimeException(sprintf(
+ 'Middleware "%s" was not found.',
+ $middleware
+ ));
+ }
+ $middleware = new $className();
+ }
+
+ if ($middleware instanceof MiddlewareInterface) {
+ return $middleware;
+ }
+
+ if (!$middleware instanceof Closure) {
+ return new DoublePassDecoratorMiddleware($middleware);
+ }
+
+ $info = new ReflectionFunction($middleware);
+ if ($info->getNumberOfParameters() > 2) {
+ return new DoublePassDecoratorMiddleware($middleware);
+ }
+
+ return new ClosureDecoratorMiddleware($middleware);
+ }
+
+ /**
+ * Append a middleware to the end of the queue.
+ *
+ * @param string|array|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware(s) to append.
+ * @return $this
+ */
+ public function add($middleware)
+ {
+ if (is_array($middleware)) {
+ $this->queue = array_merge($this->queue, $middleware);
+
+ return $this;
+ }
+ $this->queue[] = $middleware;
+
+ return $this;
+ }
+
+ /**
+ * Alias for MiddlewareQueue::add().
+ *
+ * @param string|array|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware(s) to append.
+ * @return $this
+ * @see MiddlewareQueue::add()
+ */
+ public function push($middleware)
+ {
+ return $this->add($middleware);
+ }
+
+ /**
+ * Prepend a middleware to the start of the queue.
+ *
+ * @param string|array|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware(s) to prepend.
+ * @return $this
+ */
+ public function prepend($middleware)
+ {
+ if (is_array($middleware)) {
+ $this->queue = array_merge($middleware, $this->queue);
+
+ return $this;
+ }
+ array_unshift($this->queue, $middleware);
+
+ return $this;
+ }
+
+ /**
+ * Insert a middleware at a specific index.
+ *
+ * If the index already exists, the new middleware will be inserted,
+ * and the existing element will be shifted one index greater.
+ *
+ * @param int $index The index to insert at.
+ * @param string|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware to insert.
+ * @return $this
+ */
+ public function insertAt(int $index, $middleware)
+ {
+ array_splice($this->queue, $index, 0, [$middleware]);
+
+ return $this;
+ }
+
+ /**
+ * Insert a middleware before the first matching class.
+ *
+ * Finds the index of the first middleware that matches the provided class,
+ * and inserts the supplied middleware before it.
+ *
+ * @param string $class The classname to insert the middleware before.
+ * @param string|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware to insert.
+ * @return $this
+ * @throws \LogicException If middleware to insert before is not found.
+ */
+ public function insertBefore(string $class, $middleware)
+ {
+ $found = false;
+ $i = 0;
+ foreach ($this->queue as $i => $object) {
+ if (
+ (
+ is_string($object)
+ && $object === $class
+ )
+ || is_a($object, $class)
+ ) {
+ $found = true;
+ break;
+ }
+ }
+ if ($found) {
+ return $this->insertAt($i, $middleware);
+ }
+ throw new LogicException(sprintf("No middleware matching '%s' could be found.", $class));
+ }
+
+ /**
+ * Insert a middleware object after the first matching class.
+ *
+ * Finds the index of the first middleware that matches the provided class,
+ * and inserts the supplied middleware after it. If the class is not found,
+ * this method will behave like add().
+ *
+ * @param string $class The classname to insert the middleware before.
+ * @param string|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware to insert.
+ * @return $this
+ */
+ public function insertAfter(string $class, $middleware)
+ {
+ $found = false;
+ $i = 0;
+ foreach ($this->queue as $i => $object) {
+ if (
+ (
+ is_string($object)
+ && $object === $class
+ )
+ || is_a($object, $class)
+ ) {
+ $found = true;
+ break;
+ }
+ }
+ if ($found) {
+ return $this->insertAt($i + 1, $middleware);
+ }
+
+ return $this->add($middleware);
+ }
+
+ /**
+ * Get the number of connected middleware layers.
+ *
+ * Implement the Countable interface.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->queue);
+ }
+
+ /**
+ * Seeks to a given position in the queue.
+ *
+ * @param int $position The position to seek to.
+ * @return void
+ * @see \SeekableIterator::seek()
+ */
+ public function seek($position): void
+ {
+ if (!isset($this->queue[$position])) {
+ throw new OutOfBoundsException("Invalid seek position ($position)");
+ }
+
+ $this->position = $position;
+ }
+
+ /**
+ * Rewinds back to the first element of the queue.
+ *
+ * @return void
+ * @see \Iterator::rewind()
+ */
+ public function rewind(): void
+ {
+ $this->position = 0;
+ }
+
+ /**
+ * Returns the current middleware.
+ *
+ * @return \Psr\Http\Server\MiddlewareInterface
+ * @see \Iterator::current()
+ */
+ public function current(): MiddlewareInterface
+ {
+ if (!isset($this->queue[$this->position])) {
+ throw new OutOfBoundsException("Invalid current position ($this->position)");
+ }
+
+ if ($this->queue[$this->position] instanceof MiddlewareInterface) {
+ return $this->queue[$this->position];
+ }
+
+ return $this->queue[$this->position] = $this->resolve($this->queue[$this->position]);
+ }
+
+ /**
+ * Return the key of the middleware.
+ *
+ * @return int
+ * @see \Iterator::key()
+ */
+ public function key(): int
+ {
+ return $this->position;
+ }
+
+ /**
+ * Moves the current position to the next middleware.
+ *
+ * @return void
+ * @see \Iterator::next()
+ */
+ public function next(): void
+ {
+ ++$this->position;
+ }
+
+ /**
+ * Checks if current position is valid.
+ *
+ * @return bool
+ * @see \Iterator::valid()
+ */
+ public function valid(): bool
+ {
+ return isset($this->queue[$this->position]);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/README.md b/app/vendor/cakephp/cakephp/src/Http/README.md
new file mode 100644
index 000000000..f305fdd5c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/README.md
@@ -0,0 +1,112 @@
+[](https://packagist.org/packages/cakephp/http)
+[](LICENSE.txt)
+
+# CakePHP Http Library
+
+This library provides a PSR-15 Http middleware server, PSR-7 Request and
+Response objects, and a PSR-18 Http Client. Together these classes let you
+handle incoming server requests and send outgoing HTTP requests.
+
+## Using the Http Client
+
+Sending requests is straight forward. Doing a GET request looks like:
+
+```php
+use Cake\Http\Client;
+
+$http = new Client();
+
+// Simple get
+$response = $http->get('http://example.com/test.html');
+
+// Simple get with querystring
+$response = $http->get('http://example.com/search', ['q' => 'widget']);
+
+// Simple get with querystring & additional headers
+$response = $http->get('http://example.com/search', ['q' => 'widget'], [
+ 'headers' => ['X-Requested-With' => 'XMLHttpRequest'],
+]);
+```
+
+To learn more read the [Http Client documentation](https://book.cakephp.org/4/en/core-libraries/httpclient.html).
+
+## Using the Http Server
+
+The Http Server allows an `HttpApplicationInterface` to process requests and
+emit responses. To get started first implement the
+`Cake\Http\HttpApplicationInterface` A minimal example could look like:
+
+```php
+namespace App;
+
+use Cake\Core\HttpApplicationInterface;
+use Cake\Http\MiddlewareQueue;
+use Cake\Http\Response;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+
+class Application implements HttpApplicationInterface
+{
+ /**
+ * Load all the application configuration and bootstrap logic.
+ *
+ * @return void
+ */
+ public function bootstrap(): void
+ {
+ // Load configuration here. This is the first
+ // method Cake\Http\Server will call on your application.
+ }
+
+ /**
+ * Define the HTTP middleware layers for an application.
+ *
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to set in your App Class
+ * @return \Cake\Http\MiddlewareQueue
+ */
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Add middleware for your application.
+ return $middlewareQueue;
+ }
+
+ /**
+ * Handle incoming server request and return a response.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ return new Response(['body'=>'Hello World!']);
+ }
+}
+```
+
+Once you have an application with some middleware. You can start accepting
+requests. In your application's webroot, you can add an `index.php` and process
+requests:
+
+```php
+emit($server->run());
+```
+
+You can then run your application using PHP's built in webserver:
+
+```bash
+php -S localhost:8765 -t ./webroot ./webroot/index.php
+```
+
+For more information on middleware, [consult the
+documentation](https://book.cakephp.org/4/en/controllers/middleware.html)
diff --git a/app/vendor/cakephp/cakephp/src/Http/Response.php b/app/vendor/cakephp/cakephp/src/Http/Response.php
new file mode 100644
index 000000000..385bdee9d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Response.php
@@ -0,0 +1,1581 @@
+ 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-status',
+ 208 => 'Already Reported',
+ 226 => 'IM used',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => '(Unused)',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested range not satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => 'I\'m a teapot',
+ 421 => 'Misdirected Request',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'Unordered Collection',
+ 426 => 'Upgrade Required',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 431 => 'Request Header Fields Too Large',
+ 444 => 'Connection Closed Without Response',
+ 451 => 'Unavailable For Legal Reasons',
+ 499 => 'Client Closed Request',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'Unsupported Version',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 510 => 'Not Extended',
+ 511 => 'Network Authentication Required',
+ 599 => 'Network Connect Timeout Error',
+ ];
+
+ /**
+ * Holds type key to mime type mappings for known mime types.
+ *
+ * @var array
+ */
+ protected $_mimeTypes = [
+ 'html' => ['text/html', '*/*'],
+ 'json' => 'application/json',
+ 'xml' => ['application/xml', 'text/xml'],
+ 'xhtml' => ['application/xhtml+xml', 'application/xhtml', 'text/xhtml'],
+ 'webp' => 'image/webp',
+ 'rss' => 'application/rss+xml',
+ 'ai' => 'application/postscript',
+ 'bcpio' => 'application/x-bcpio',
+ 'bin' => 'application/octet-stream',
+ 'ccad' => 'application/clariscad',
+ 'cdf' => 'application/x-netcdf',
+ 'class' => 'application/octet-stream',
+ 'cpio' => 'application/x-cpio',
+ 'cpt' => 'application/mac-compactpro',
+ 'csh' => 'application/x-csh',
+ 'csv' => ['text/csv', 'application/vnd.ms-excel'],
+ 'dcr' => 'application/x-director',
+ 'dir' => 'application/x-director',
+ 'dms' => 'application/octet-stream',
+ 'doc' => 'application/msword',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'drw' => 'application/drafting',
+ 'dvi' => 'application/x-dvi',
+ 'dwg' => 'application/acad',
+ 'dxf' => 'application/dxf',
+ 'dxr' => 'application/x-director',
+ 'eot' => 'application/vnd.ms-fontobject',
+ 'eps' => 'application/postscript',
+ 'exe' => 'application/octet-stream',
+ 'ez' => 'application/andrew-inset',
+ 'flv' => 'video/x-flv',
+ 'gtar' => 'application/x-gtar',
+ 'gz' => 'application/x-gzip',
+ 'bz2' => 'application/x-bzip',
+ '7z' => 'application/x-7z-compressed',
+ 'haljson' => ['application/hal+json', 'application/vnd.hal+json'],
+ 'halxml' => ['application/hal+xml', 'application/vnd.hal+xml'],
+ 'hdf' => 'application/x-hdf',
+ 'hqx' => 'application/mac-binhex40',
+ 'ico' => 'image/x-icon',
+ 'ips' => 'application/x-ipscript',
+ 'ipx' => 'application/x-ipix',
+ 'js' => 'application/javascript',
+ 'jsonapi' => 'application/vnd.api+json',
+ 'latex' => 'application/x-latex',
+ 'jsonld' => 'application/ld+json',
+ 'lha' => 'application/octet-stream',
+ 'lsp' => 'application/x-lisp',
+ 'lzh' => 'application/octet-stream',
+ 'man' => 'application/x-troff-man',
+ 'me' => 'application/x-troff-me',
+ 'mif' => 'application/vnd.mif',
+ 'ms' => 'application/x-troff-ms',
+ 'nc' => 'application/x-netcdf',
+ 'oda' => 'application/oda',
+ 'otf' => 'font/otf',
+ 'pdf' => 'application/pdf',
+ 'pgn' => 'application/x-chess-pgn',
+ 'pot' => 'application/vnd.ms-powerpoint',
+ 'pps' => 'application/vnd.ms-powerpoint',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'ppz' => 'application/vnd.ms-powerpoint',
+ 'pre' => 'application/x-freelance',
+ 'prt' => 'application/pro_eng',
+ 'ps' => 'application/postscript',
+ 'roff' => 'application/x-troff',
+ 'scm' => 'application/x-lotusscreencam',
+ 'set' => 'application/set',
+ 'sh' => 'application/x-sh',
+ 'shar' => 'application/x-shar',
+ 'sit' => 'application/x-stuffit',
+ 'skd' => 'application/x-koan',
+ 'skm' => 'application/x-koan',
+ 'skp' => 'application/x-koan',
+ 'skt' => 'application/x-koan',
+ 'smi' => 'application/smil',
+ 'smil' => 'application/smil',
+ 'sol' => 'application/solids',
+ 'spl' => 'application/x-futuresplash',
+ 'src' => 'application/x-wais-source',
+ 'step' => 'application/STEP',
+ 'stl' => 'application/SLA',
+ 'stp' => 'application/STEP',
+ 'sv4cpio' => 'application/x-sv4cpio',
+ 'sv4crc' => 'application/x-sv4crc',
+ 'svg' => 'image/svg+xml',
+ 'svgz' => 'image/svg+xml',
+ 'swf' => 'application/x-shockwave-flash',
+ 't' => 'application/x-troff',
+ 'tar' => 'application/x-tar',
+ 'tcl' => 'application/x-tcl',
+ 'tex' => 'application/x-tex',
+ 'texi' => 'application/x-texinfo',
+ 'texinfo' => 'application/x-texinfo',
+ 'tr' => 'application/x-troff',
+ 'tsp' => 'application/dsptype',
+ 'ttc' => 'font/ttf',
+ 'ttf' => 'font/ttf',
+ 'unv' => 'application/i-deas',
+ 'ustar' => 'application/x-ustar',
+ 'vcd' => 'application/x-cdlink',
+ 'vda' => 'application/vda',
+ 'xlc' => 'application/vnd.ms-excel',
+ 'xll' => 'application/vnd.ms-excel',
+ 'xlm' => 'application/vnd.ms-excel',
+ 'xls' => 'application/vnd.ms-excel',
+ 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'xlw' => 'application/vnd.ms-excel',
+ 'zip' => 'application/zip',
+ 'aif' => 'audio/x-aiff',
+ 'aifc' => 'audio/x-aiff',
+ 'aiff' => 'audio/x-aiff',
+ 'au' => 'audio/basic',
+ 'kar' => 'audio/midi',
+ 'mid' => 'audio/midi',
+ 'midi' => 'audio/midi',
+ 'mp2' => 'audio/mpeg',
+ 'mp3' => 'audio/mpeg',
+ 'mpga' => 'audio/mpeg',
+ 'ogg' => 'audio/ogg',
+ 'oga' => 'audio/ogg',
+ 'spx' => 'audio/ogg',
+ 'ra' => 'audio/x-realaudio',
+ 'ram' => 'audio/x-pn-realaudio',
+ 'rm' => 'audio/x-pn-realaudio',
+ 'rpm' => 'audio/x-pn-realaudio-plugin',
+ 'snd' => 'audio/basic',
+ 'tsi' => 'audio/TSP-audio',
+ 'wav' => 'audio/x-wav',
+ 'aac' => 'audio/aac',
+ 'asc' => 'text/plain',
+ 'c' => 'text/plain',
+ 'cc' => 'text/plain',
+ 'css' => 'text/css',
+ 'etx' => 'text/x-setext',
+ 'f' => 'text/plain',
+ 'f90' => 'text/plain',
+ 'h' => 'text/plain',
+ 'hh' => 'text/plain',
+ 'htm' => ['text/html', '*/*'],
+ 'ics' => 'text/calendar',
+ 'm' => 'text/plain',
+ 'rtf' => 'text/rtf',
+ 'rtx' => 'text/richtext',
+ 'sgm' => 'text/sgml',
+ 'sgml' => 'text/sgml',
+ 'tsv' => 'text/tab-separated-values',
+ 'tpl' => 'text/template',
+ 'txt' => 'text/plain',
+ 'text' => 'text/plain',
+ 'avi' => 'video/x-msvideo',
+ 'fli' => 'video/x-fli',
+ 'mov' => 'video/quicktime',
+ 'movie' => 'video/x-sgi-movie',
+ 'mpe' => 'video/mpeg',
+ 'mpeg' => 'video/mpeg',
+ 'mpg' => 'video/mpeg',
+ 'qt' => 'video/quicktime',
+ 'viv' => 'video/vnd.vivo',
+ 'vivo' => 'video/vnd.vivo',
+ 'ogv' => 'video/ogg',
+ 'webm' => 'video/webm',
+ 'mp4' => 'video/mp4',
+ 'm4v' => 'video/mp4',
+ 'f4v' => 'video/mp4',
+ 'f4p' => 'video/mp4',
+ 'm4a' => 'audio/mp4',
+ 'f4a' => 'audio/mp4',
+ 'f4b' => 'audio/mp4',
+ 'gif' => 'image/gif',
+ 'ief' => 'image/ief',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'jpe' => 'image/jpeg',
+ 'pbm' => 'image/x-portable-bitmap',
+ 'pgm' => 'image/x-portable-graymap',
+ 'png' => 'image/png',
+ 'pnm' => 'image/x-portable-anymap',
+ 'ppm' => 'image/x-portable-pixmap',
+ 'ras' => 'image/cmu-raster',
+ 'rgb' => 'image/x-rgb',
+ 'tif' => 'image/tiff',
+ 'tiff' => 'image/tiff',
+ 'xbm' => 'image/x-xbitmap',
+ 'xpm' => 'image/x-xpixmap',
+ 'xwd' => 'image/x-xwindowdump',
+ 'psd' => [
+ 'application/photoshop',
+ 'application/psd',
+ 'image/psd',
+ 'image/x-photoshop',
+ 'image/photoshop',
+ 'zz-application/zz-winassoc-psd',
+ ],
+ 'ice' => 'x-conference/x-cooltalk',
+ 'iges' => 'model/iges',
+ 'igs' => 'model/iges',
+ 'mesh' => 'model/mesh',
+ 'msh' => 'model/mesh',
+ 'silo' => 'model/mesh',
+ 'vrml' => 'model/vrml',
+ 'wrl' => 'model/vrml',
+ 'mime' => 'www/mime',
+ 'pdb' => 'chemical/x-pdb',
+ 'xyz' => 'chemical/x-pdb',
+ 'javascript' => 'application/javascript',
+ 'form' => 'application/x-www-form-urlencoded',
+ 'file' => 'multipart/form-data',
+ 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
+ 'atom' => 'application/atom+xml',
+ 'amf' => 'application/x-amf',
+ 'wap' => ['text/vnd.wap.wml', 'text/vnd.wap.wmlscript', 'image/vnd.wap.wbmp'],
+ 'wml' => 'text/vnd.wap.wml',
+ 'wmlscript' => 'text/vnd.wap.wmlscript',
+ 'wbmp' => 'image/vnd.wap.wbmp',
+ 'woff' => 'application/x-font-woff',
+ 'appcache' => 'text/cache-manifest',
+ 'manifest' => 'text/cache-manifest',
+ 'htc' => 'text/x-component',
+ 'rdf' => 'application/xml',
+ 'crx' => 'application/x-chrome-extension',
+ 'oex' => 'application/x-opera-extension',
+ 'xpi' => 'application/x-xpinstall',
+ 'safariextz' => 'application/octet-stream',
+ 'webapp' => 'application/x-web-app-manifest+json',
+ 'vcf' => 'text/x-vcard',
+ 'vtt' => 'text/vtt',
+ 'mkv' => 'video/x-matroska',
+ 'pkpass' => 'application/vnd.apple.pkpass',
+ 'ajax' => 'text/html',
+ 'bmp' => 'image/bmp',
+ ];
+
+ /**
+ * Status code to send to the client
+ *
+ * @var int
+ */
+ protected $_status = 200;
+
+ /**
+ * File object for file to be read out as response
+ *
+ * @var \SplFileInfo|null
+ */
+ protected $_file;
+
+ /**
+ * File range. Used for requesting ranges of files.
+ *
+ * @var array
+ */
+ protected $_fileRange = [];
+
+ /**
+ * The charset the response body is encoded with
+ *
+ * @var string
+ */
+ protected $_charset = 'UTF-8';
+
+ /**
+ * Holds all the cache directives that will be converted
+ * into headers when sending the request
+ *
+ * @var array
+ */
+ protected $_cacheDirectives = [];
+
+ /**
+ * Collection of cookies to send to the client
+ *
+ * @var \Cake\Http\Cookie\CookieCollection
+ */
+ protected $_cookies;
+
+ /**
+ * Reason Phrase
+ *
+ * @var string
+ */
+ protected $_reasonPhrase = 'OK';
+
+ /**
+ * Stream mode options.
+ *
+ * @var string
+ */
+ protected $_streamMode = 'wb+';
+
+ /**
+ * Stream target or resource object.
+ *
+ * @var string|resource
+ */
+ protected $_streamTarget = 'php://memory';
+
+ /**
+ * Constructor
+ *
+ * @param array $options list of parameters to setup the response. Possible values are:
+ *
+ * - body: the response text that should be sent to the client
+ * - status: the HTTP status code to respond with
+ * - type: a complete mime-type string or an extension mapped in this class
+ * - charset: the charset for the response body
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(array $options = [])
+ {
+ if (isset($options['streamTarget'])) {
+ $this->_streamTarget = $options['streamTarget'];
+ }
+ if (isset($options['streamMode'])) {
+ $this->_streamMode = $options['streamMode'];
+ }
+ if (isset($options['stream'])) {
+ if (!$options['stream'] instanceof StreamInterface) {
+ throw new InvalidArgumentException('Stream option must be an object that implements StreamInterface');
+ }
+ $this->stream = $options['stream'];
+ } else {
+ $this->_createStream();
+ }
+ if (isset($options['body'])) {
+ $this->stream->write($options['body']);
+ }
+ if (isset($options['status'])) {
+ $this->_setStatus($options['status']);
+ }
+ if (!isset($options['charset'])) {
+ $options['charset'] = Configure::read('App.encoding');
+ }
+ $this->_charset = $options['charset'];
+ $type = 'text/html';
+ if (isset($options['type'])) {
+ $type = $this->resolveType($options['type']);
+ }
+ $this->_setContentType($type);
+ $this->_cookies = new CookieCollection();
+ }
+
+ /**
+ * Creates the stream object.
+ *
+ * @return void
+ */
+ protected function _createStream(): void
+ {
+ $this->stream = new Stream($this->_streamTarget, $this->_streamMode);
+ }
+
+ /**
+ * Formats the Content-Type header based on the configured contentType and charset
+ * the charset will only be set in the header if the response is of type text/*
+ *
+ * @param string $type The type to set.
+ * @return void
+ */
+ protected function _setContentType(string $type): void
+ {
+ if (in_array($this->_status, [304, 204], true)) {
+ $this->_clearHeader('Content-Type');
+
+ return;
+ }
+ $allowed = [
+ 'application/javascript', 'application/xml', 'application/rss+xml',
+ ];
+
+ $charset = false;
+ if (
+ $this->_charset &&
+ (
+ strpos($type, 'text/') === 0 ||
+ in_array($type, $allowed, true)
+ )
+ ) {
+ $charset = true;
+ }
+
+ if ($charset && strpos($type, ';') === false) {
+ $this->_setHeader('Content-Type', "{$type}; charset={$this->_charset}");
+ } else {
+ $this->_setHeader('Content-Type', $type);
+ }
+ }
+
+ /**
+ * Return an instance with an updated location header.
+ *
+ * If the current status code is 200, it will be replaced
+ * with 302.
+ *
+ * @param string $url The location to redirect to.
+ * @return static A new response with the Location header set.
+ */
+ public function withLocation(string $url)
+ {
+ $new = $this->withHeader('Location', $url);
+ if ($new->_status === 200) {
+ $new->_status = 302;
+ }
+
+ return $new;
+ }
+
+ /**
+ * Sets a header.
+ *
+ * @param string $header Header key.
+ * @param string $value Header value.
+ * @return void
+ */
+ protected function _setHeader(string $header, string $value): void
+ {
+ $normalized = strtolower($header);
+ $this->headerNames[$normalized] = $header;
+ $this->headers[$header] = [$value];
+ }
+
+ /**
+ * Clear header
+ *
+ * @param string $header Header key.
+ * @return void
+ */
+ protected function _clearHeader(string $header): void
+ {
+ $normalized = strtolower($header);
+ if (!isset($this->headerNames[$normalized])) {
+ return;
+ }
+ $original = $this->headerNames[$normalized];
+ unset($this->headerNames[$normalized], $this->headers[$original]);
+ }
+
+ /**
+ * Gets the response status code.
+ *
+ * The status code is a 3-digit integer result code of the server's attempt
+ * to understand and satisfy the request.
+ *
+ * @return int Status code.
+ */
+ public function getStatusCode(): int
+ {
+ return $this->_status;
+ }
+
+ /**
+ * Return an instance with the specified status code and, optionally, reason phrase.
+ *
+ * If no reason phrase is specified, implementations MAY choose to default
+ * to the RFC 7231 or IANA recommended reason phrase for the response's
+ * status code.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated status and reason phrase.
+ *
+ * If the status code is 304 or 204, the existing Content-Type header
+ * will be cleared, as these response codes have no body.
+ *
+ * There are external packages such as `fig/http-message-util` that provide HTTP
+ * status code constants. These can be used with any method that accepts or
+ * returns a status code integer. However, keep in mind that these constants
+ * might include status codes that are now allowed which will throw an
+ * `\InvalidArgumentException`.
+ *
+ * @link https://tools.ietf.org/html/rfc7231#section-6
+ * @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @param int $code The 3-digit integer status code to set.
+ * @param string $reasonPhrase The reason phrase to use with the
+ * provided status code; if none is provided, implementations MAY
+ * use the defaults as suggested in the HTTP specification.
+ * @return static
+ * @throws \InvalidArgumentException For invalid status code arguments.
+ */
+ public function withStatus($code, $reasonPhrase = '')
+ {
+ $new = clone $this;
+ $new->_setStatus($code, $reasonPhrase);
+
+ return $new;
+ }
+
+ /**
+ * Modifier for response status
+ *
+ * @param int $code The status code to set.
+ * @param string $reasonPhrase The response reason phrase.
+ * @return void
+ * @throws \InvalidArgumentException For invalid status code arguments.
+ */
+ protected function _setStatus(int $code, string $reasonPhrase = ''): void
+ {
+ if ($code < static::STATUS_CODE_MIN || $code > static::STATUS_CODE_MAX) {
+ throw new InvalidArgumentException(sprintf(
+ 'Invalid status code: %s. Use a valid HTTP status code in range 1xx - 5xx.',
+ $code
+ ));
+ }
+
+ $this->_status = $code;
+ if ($reasonPhrase === '' && isset($this->_statusCodes[$code])) {
+ $reasonPhrase = $this->_statusCodes[$code];
+ }
+ $this->_reasonPhrase = $reasonPhrase;
+
+ // These status codes don't have bodies and can't have content-types.
+ if (in_array($code, [304, 204], true)) {
+ $this->_clearHeader('Content-Type');
+ }
+ }
+
+ /**
+ * Gets the response reason phrase associated with the status code.
+ *
+ * Because a reason phrase is not a required element in a response
+ * status line, the reason phrase value MAY be null. Implementations MAY
+ * choose to return the default RFC 7231 recommended reason phrase (or those
+ * listed in the IANA HTTP Status Code Registry) for the response's
+ * status code.
+ *
+ * @link https://tools.ietf.org/html/rfc7231#section-6
+ * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ * @return string Reason phrase; must return an empty string if none present.
+ */
+ public function getReasonPhrase(): string
+ {
+ return $this->_reasonPhrase;
+ }
+
+ /**
+ * Sets a content type definition into the map.
+ *
+ * E.g.: setTypeMap('xhtml', ['application/xhtml+xml', 'application/xhtml'])
+ *
+ * This is needed for RequestHandlerComponent and recognition of types.
+ *
+ * @param string $type Content type.
+ * @param string|array $mimeType Definition of the mime type.
+ * @return void
+ */
+ public function setTypeMap(string $type, $mimeType): void
+ {
+ $this->_mimeTypes[$type] = $mimeType;
+ }
+
+ /**
+ * Returns the current content type.
+ *
+ * @return string
+ */
+ public function getType(): string
+ {
+ $header = $this->getHeaderLine('Content-Type');
+ if (strpos($header, ';') !== false) {
+ return explode(';', $header)[0];
+ }
+
+ return $header;
+ }
+
+ /**
+ * Get an updated response with the content type set.
+ *
+ * If you attempt to set the type on a 304 or 204 status code response, the
+ * content type will not take effect as these status codes do not have content-types.
+ *
+ * @param string $contentType Either a file extension which will be mapped to a mime-type or a concrete mime-type.
+ * @return static
+ */
+ public function withType(string $contentType)
+ {
+ $mappedType = $this->resolveType($contentType);
+ $new = clone $this;
+ $new->_setContentType($mappedType);
+
+ return $new;
+ }
+
+ /**
+ * Translate and validate content-types.
+ *
+ * @param string $contentType The content-type or type alias.
+ * @return string The resolved content-type
+ * @throws \InvalidArgumentException When an invalid content-type or alias is used.
+ */
+ protected function resolveType(string $contentType): string
+ {
+ $mapped = $this->getMimeType($contentType);
+ if ($mapped) {
+ return is_array($mapped) ? current($mapped) : $mapped;
+ }
+ if (strpos($contentType, '/') === false) {
+ throw new InvalidArgumentException(sprintf('"%s" is an invalid content type.', $contentType));
+ }
+
+ return $contentType;
+ }
+
+ /**
+ * Returns the mime type definition for an alias
+ *
+ * e.g `getMimeType('pdf'); // returns 'application/pdf'`
+ *
+ * @param string $alias the content type alias to map
+ * @return string|array|false String mapped mime type or false if $alias is not mapped
+ */
+ public function getMimeType(string $alias)
+ {
+ if (isset($this->_mimeTypes[$alias])) {
+ return $this->_mimeTypes[$alias];
+ }
+
+ return false;
+ }
+
+ /**
+ * Maps a content-type back to an alias
+ *
+ * e.g `mapType('application/pdf'); // returns 'pdf'`
+ *
+ * @param string|array $ctype Either a string content type to map, or an array of types.
+ * @return string|array|null Aliases for the types provided.
+ */
+ public function mapType($ctype)
+ {
+ if (is_array($ctype)) {
+ return array_map([$this, 'mapType'], $ctype);
+ }
+
+ foreach ($this->_mimeTypes as $alias => $types) {
+ if (in_array($ctype, (array)$types, true)) {
+ return $alias;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the current charset.
+ *
+ * @return string
+ */
+ public function getCharset(): string
+ {
+ return $this->_charset;
+ }
+
+ /**
+ * Get a new instance with an updated charset.
+ *
+ * @param string $charset Character set string.
+ * @return static
+ */
+ public function withCharset(string $charset)
+ {
+ $new = clone $this;
+ $new->_charset = $charset;
+ $new->_setContentType($this->getType());
+
+ return $new;
+ }
+
+ /**
+ * Create a new instance with headers to instruct the client to not cache the response
+ *
+ * @return static
+ */
+ public function withDisabledCache()
+ {
+ return $this->withHeader('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
+ ->withHeader('Last-Modified', gmdate(DATE_RFC7231))
+ ->withHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
+ }
+
+ /**
+ * Create a new instance with the headers to enable client caching.
+ *
+ * @param int|string $since a valid time since the response text has not been modified
+ * @param int|string $time a valid time for cache expiry
+ * @return static
+ */
+ public function withCache($since, $time = '+1 day')
+ {
+ if (!is_int($time)) {
+ $time = strtotime($time);
+ if ($time === false) {
+ throw new InvalidArgumentException(
+ 'Invalid time parameter. Ensure your time value can be parsed by strtotime'
+ );
+ }
+ }
+
+ return $this->withHeader('Date', gmdate(DATE_RFC7231, time()))
+ ->withModified($since)
+ ->withExpires($time)
+ ->withSharable(true)
+ ->withMaxAge($time - time());
+ }
+
+ /**
+ * Create a new instace with the public/private Cache-Control directive set.
+ *
+ * @param bool $public If set to true, the Cache-Control header will be set as public
+ * if set to false, the response will be set to private.
+ * @param int|null $time time in seconds after which the response should no longer be considered fresh.
+ * @return static
+ */
+ public function withSharable(bool $public, ?int $time = null)
+ {
+ $new = clone $this;
+ unset($new->_cacheDirectives['private'], $new->_cacheDirectives['public']);
+
+ $key = $public ? 'public' : 'private';
+ $new->_cacheDirectives[$key] = true;
+
+ if ($time !== null) {
+ $new->_cacheDirectives['max-age'] = $time;
+ }
+ $new->_setCacheControl();
+
+ return $new;
+ }
+
+ /**
+ * Create a new instance with the Cache-Control s-maxage directive.
+ *
+ * The max-age is the number of seconds after which the response should no longer be considered
+ * a good candidate to be fetched from a shared cache (like in a proxy server).
+ *
+ * @param int $seconds The number of seconds for shared max-age
+ * @return static
+ */
+ public function withSharedMaxAge(int $seconds)
+ {
+ $new = clone $this;
+ $new->_cacheDirectives['s-maxage'] = $seconds;
+ $new->_setCacheControl();
+
+ return $new;
+ }
+
+ /**
+ * Create an instance with Cache-Control max-age directive set.
+ *
+ * The max-age is the number of seconds after which the response should no longer be considered
+ * a good candidate to be fetched from the local (client) cache.
+ *
+ * @param int $seconds The seconds a cached response can be considered valid
+ * @return static
+ */
+ public function withMaxAge(int $seconds)
+ {
+ $new = clone $this;
+ $new->_cacheDirectives['max-age'] = $seconds;
+ $new->_setCacheControl();
+
+ return $new;
+ }
+
+ /**
+ * Create an instance with Cache-Control must-revalidate directive set.
+ *
+ * Sets the Cache-Control must-revalidate directive.
+ * must-revalidate indicates that the response should not be served
+ * stale by a cache under any circumstance without first revalidating
+ * with the origin.
+ *
+ * @param bool $enable If boolean sets or unsets the directive.
+ * @return static
+ */
+ public function withMustRevalidate(bool $enable)
+ {
+ $new = clone $this;
+ if ($enable) {
+ $new->_cacheDirectives['must-revalidate'] = true;
+ } else {
+ unset($new->_cacheDirectives['must-revalidate']);
+ }
+ $new->_setCacheControl();
+
+ return $new;
+ }
+
+ /**
+ * Helper method to generate a valid Cache-Control header from the options set
+ * in other methods
+ *
+ * @return void
+ */
+ protected function _setCacheControl(): void
+ {
+ $control = '';
+ foreach ($this->_cacheDirectives as $key => $val) {
+ $control .= $val === true ? $key : sprintf('%s=%s', $key, $val);
+ $control .= ', ';
+ }
+ $control = rtrim($control, ', ');
+ $this->_setHeader('Cache-Control', $control);
+ }
+
+ /**
+ * Create a new instance with the Expires header set.
+ *
+ * ### Examples:
+ *
+ * ```
+ * // Will Expire the response cache now
+ * $response->withExpires('now')
+ *
+ * // Will set the expiration in next 24 hours
+ * $response->withExpires(new DateTime('+1 day'))
+ * ```
+ *
+ * @param string|int|\DateTimeInterface|null $time Valid time string or \DateTime instance.
+ * @return static
+ */
+ public function withExpires($time)
+ {
+ $date = $this->_getUTCDate($time);
+
+ return $this->withHeader('Expires', $date->format(DATE_RFC7231));
+ }
+
+ /**
+ * Create a new instance with the Last-Modified header set.
+ *
+ * ### Examples:
+ *
+ * ```
+ * // Will Expire the response cache now
+ * $response->withModified('now')
+ *
+ * // Will set the expiration in next 24 hours
+ * $response->withModified(new DateTime('+1 day'))
+ * ```
+ *
+ * @param int|string|\DateTimeInterface $time Valid time string or \DateTime instance.
+ * @return static
+ */
+ public function withModified($time)
+ {
+ $date = $this->_getUTCDate($time);
+
+ return $this->withHeader('Last-Modified', $date->format(DATE_RFC7231));
+ }
+
+ /**
+ * Sets the response as Not Modified by removing any body contents
+ * setting the status code to "304 Not Modified" and removing all
+ * conflicting headers
+ *
+ * *Warning* This method mutates the response in-place and should be avoided.
+ *
+ * @return void
+ */
+ public function notModified(): void
+ {
+ $this->_createStream();
+ $this->_setStatus(304);
+
+ $remove = [
+ 'Allow',
+ 'Content-Encoding',
+ 'Content-Language',
+ 'Content-Length',
+ 'Content-MD5',
+ 'Content-Type',
+ 'Last-Modified',
+ ];
+ foreach ($remove as $header) {
+ $this->_clearHeader($header);
+ }
+ }
+
+ /**
+ * Create a new instance as 'not modified'
+ *
+ * This will remove any body contents set the status code
+ * to "304" and removing headers that describe
+ * a response body.
+ *
+ * @return static
+ */
+ public function withNotModified()
+ {
+ $new = $this->withStatus(304);
+ $new->_createStream();
+ $remove = [
+ 'Allow',
+ 'Content-Encoding',
+ 'Content-Language',
+ 'Content-Length',
+ 'Content-MD5',
+ 'Content-Type',
+ 'Last-Modified',
+ ];
+ foreach ($remove as $header) {
+ $new = $new->withoutHeader($header);
+ }
+
+ return $new;
+ }
+
+ /**
+ * Create a new instance with the Vary header set.
+ *
+ * If an array is passed values will be imploded into a comma
+ * separated string. If no parameters are passed, then an
+ * array with the current Vary header value is returned
+ *
+ * @param string|array $cacheVariances A single Vary string or an array
+ * containing the list for variances.
+ * @return static
+ */
+ public function withVary($cacheVariances)
+ {
+ return $this->withHeader('Vary', (array)$cacheVariances);
+ }
+
+ /**
+ * Create a new instance with the Etag header set.
+ *
+ * Etags are a strong indicative that a response can be cached by a
+ * HTTP client. A bad way of generating Etags is creating a hash of
+ * the response output, instead generate a unique hash of the
+ * unique components that identifies a request, such as a
+ * modification time, a resource Id, and anything else you consider it
+ * that makes the response unique.
+ *
+ * The second parameter is used to inform clients that the content has
+ * changed, but semantically it is equivalent to existing cached values. Consider
+ * a page with a hit counter, two different page views are equivalent, but
+ * they differ by a few bytes. This permits the Client to decide whether they should
+ * use the cached data.
+ *
+ * @param string $hash The unique hash that identifies this response
+ * @param bool $weak Whether the response is semantically the same as
+ * other with the same hash or not. Defaults to false
+ * @return static
+ */
+ public function withEtag(string $hash, bool $weak = false)
+ {
+ $hash = sprintf('%s"%s"', $weak ? 'W/' : '', $hash);
+
+ return $this->withHeader('Etag', $hash);
+ }
+
+ /**
+ * Returns a DateTime object initialized at the $time param and using UTC
+ * as timezone
+ *
+ * @param string|int|\DateTimeInterface|null $time Valid time string or \DateTimeInterface instance.
+ * @return \DateTimeInterface
+ */
+ protected function _getUTCDate($time = null): DateTimeInterface
+ {
+ if ($time instanceof DateTimeInterface) {
+ $result = clone $time;
+ } elseif (is_int($time)) {
+ $result = new DateTime(date('Y-m-d H:i:s', $time));
+ } else {
+ $result = new DateTime($time ?? 'now');
+ }
+
+ /** @psalm-suppress UndefinedInterfaceMethod */
+ return $result->setTimezone(new DateTimeZone('UTC'));
+ }
+
+ /**
+ * Sets the correct output buffering handler to send a compressed response. Responses will
+ * be compressed with zlib, if the extension is available.
+ *
+ * @return bool false if client does not accept compressed responses or no handler is available, true otherwise
+ */
+ public function compress(): bool
+ {
+ $compressionEnabled = ini_get('zlib.output_compression') !== '1' &&
+ extension_loaded('zlib') &&
+ (strpos((string)env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false);
+
+ return $compressionEnabled && ob_start('ob_gzhandler');
+ }
+
+ /**
+ * Returns whether the resulting output will be compressed by PHP
+ *
+ * @return bool
+ */
+ public function outputCompressed(): bool
+ {
+ return strpos((string)env('HTTP_ACCEPT_ENCODING'), 'gzip') !== false
+ && (ini_get('zlib.output_compression') === '1' || in_array('ob_gzhandler', ob_list_handlers(), true));
+ }
+
+ /**
+ * Create a new instance with the Content-Disposition header set.
+ *
+ * @param string $filename The name of the file as the browser will download the response
+ * @return static
+ */
+ public function withDownload(string $filename)
+ {
+ return $this->withHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
+ }
+
+ /**
+ * Create a new response with the Content-Length header set.
+ *
+ * @param int|string $bytes Number of bytes
+ * @return static
+ */
+ public function withLength($bytes)
+ {
+ return $this->withHeader('Content-Length', (string)$bytes);
+ }
+
+ /**
+ * Create a new response with the Link header set.
+ *
+ * ### Examples
+ *
+ * ```
+ * $response = $response->withAddedLink('http://example.com?page=1', ['rel' => 'prev'])
+ * ->withAddedLink('http://example.com?page=3', ['rel' => 'next']);
+ * ```
+ *
+ * Will generate:
+ *
+ * ```
+ * Link: ; rel="prev"
+ * Link: ; rel="next"
+ * ```
+ *
+ * @param string $url The LinkHeader url.
+ * @param array $options The LinkHeader params.
+ * @return static
+ * @since 3.6.0
+ */
+ public function withAddedLink(string $url, array $options = [])
+ {
+ $params = [];
+ foreach ($options as $key => $option) {
+ $params[] = $key . '="' . $option . '"';
+ }
+
+ $param = '';
+ if ($params) {
+ $param = '; ' . implode('; ', $params);
+ }
+
+ return $this->withAddedHeader('Link', '<' . $url . '>' . $param);
+ }
+
+ /**
+ * Checks whether a response has not been modified according to the 'If-None-Match'
+ * (Etags) and 'If-Modified-Since' (last modification date) request
+ * headers. If the response is detected to be not modified, it
+ * is marked as so accordingly so the client can be informed of that.
+ *
+ * In order to mark a response as not modified, you need to set at least
+ * the Last-Modified etag response header before calling this method. Otherwise
+ * a comparison will not be possible.
+ *
+ * *Warning* This method mutates the response in-place and should be avoided.
+ *
+ * @param \Cake\Http\ServerRequest $request Request object
+ * @return bool Whether the response was marked as not modified or not.
+ */
+ public function checkNotModified(ServerRequest $request): bool
+ {
+ $etags = preg_split('/\s*,\s*/', $request->getHeaderLine('If-None-Match'), 0, PREG_SPLIT_NO_EMPTY);
+ $responseTag = $this->getHeaderLine('Etag');
+ $etagMatches = null;
+ if ($responseTag) {
+ $etagMatches = in_array('*', $etags, true) || in_array($responseTag, $etags, true);
+ }
+
+ $modifiedSince = $request->getHeaderLine('If-Modified-Since');
+ $timeMatches = null;
+ if ($modifiedSince && $this->hasHeader('Last-Modified')) {
+ $timeMatches = strtotime($this->getHeaderLine('Last-Modified')) === strtotime($modifiedSince);
+ }
+ if ($etagMatches === null && $timeMatches === null) {
+ return false;
+ }
+ $notModified = $etagMatches !== false && $timeMatches !== false;
+ if ($notModified) {
+ $this->notModified();
+ }
+
+ return $notModified;
+ }
+
+ /**
+ * String conversion. Fetches the response body as a string.
+ * Does *not* send headers.
+ * If body is a callable, a blank string is returned.
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ $this->stream->rewind();
+
+ return $this->stream->getContents();
+ }
+
+ /**
+ * Create a new response with a cookie set.
+ *
+ * ### Example
+ *
+ * ```
+ * // add a cookie object
+ * $response = $response->withCookie(new Cookie('remember_me', 1));
+ * ```
+ *
+ * @param \Cake\Http\Cookie\CookieInterface $cookie cookie object
+ * @return static
+ */
+ public function withCookie(CookieInterface $cookie)
+ {
+ $new = clone $this;
+ $new->_cookies = $new->_cookies->add($cookie);
+
+ return $new;
+ }
+
+ /**
+ * Create a new response with an expired cookie set.
+ *
+ * ### Example
+ *
+ * ```
+ * // add a cookie object
+ * $response = $response->withExpiredCookie(new Cookie('remember_me'));
+ * ```
+ *
+ * @param \Cake\Http\Cookie\CookieInterface $cookie cookie object
+ * @return static
+ */
+ public function withExpiredCookie(CookieInterface $cookie)
+ {
+ $cookie = $cookie->withExpired();
+
+ $new = clone $this;
+ $new->_cookies = $new->_cookies->add($cookie);
+
+ return $new;
+ }
+
+ /**
+ * Read a single cookie from the response.
+ *
+ * This method provides read access to pending cookies. It will
+ * not read the `Set-Cookie` header if set.
+ *
+ * @param string $name The cookie name you want to read.
+ * @return array|null Either the cookie data or null
+ */
+ public function getCookie(string $name): ?array
+ {
+ if (!$this->_cookies->has($name)) {
+ return null;
+ }
+
+ return $this->_cookies->get($name)->toArray();
+ }
+
+ /**
+ * Get all cookies in the response.
+ *
+ * Returns an associative array of cookie name => cookie data.
+ *
+ * @return array
+ */
+ public function getCookies(): array
+ {
+ $out = [];
+ /** @var \Cake\Http\Cookie\Cookie[] $cookies */
+ $cookies = $this->_cookies;
+ foreach ($cookies as $cookie) {
+ $out[$cookie->getName()] = $cookie->toArray();
+ }
+
+ return $out;
+ }
+
+ /**
+ * Get the CookieCollection from the response
+ *
+ * @return \Cake\Http\Cookie\CookieCollection
+ */
+ public function getCookieCollection(): CookieCollection
+ {
+ return $this->_cookies;
+ }
+
+ /**
+ * Get a new instance with provided cookie collection.
+ *
+ * @param \Cake\Http\Cookie\CookieCollection $cookieCollection Cookie collection to set.
+ * @return static
+ */
+ public function withCookieCollection(CookieCollection $cookieCollection)
+ {
+ $new = clone $this;
+ $new->_cookies = $cookieCollection;
+
+ return $new;
+ }
+
+ /**
+ * Get a CorsBuilder instance for defining CORS headers.
+ *
+ * This method allow multiple ways to setup the domains, see the examples
+ *
+ * ### Full URI
+ * ```
+ * cors($request, 'https://www.cakephp.org');
+ * ```
+ *
+ * ### URI with wildcard
+ * ```
+ * cors($request, 'https://*.cakephp.org');
+ * ```
+ *
+ * ### Ignoring the requested protocol
+ * ```
+ * cors($request, 'www.cakephp.org');
+ * ```
+ *
+ * ### Any URI
+ * ```
+ * cors($request, '*');
+ * ```
+ *
+ * ### Allowed list of URIs
+ * ```
+ * cors($request, ['http://www.cakephp.org', '*.google.com', 'https://myproject.github.io']);
+ * ```
+ *
+ * *Note* The `$allowedDomains`, `$allowedMethods`, `$allowedHeaders` parameters are deprecated.
+ * Instead the builder object should be used.
+ *
+ * @param \Cake\Http\ServerRequest $request Request object
+ * @return \Cake\Http\CorsBuilder A builder object the provides a fluent interface for defining
+ * additional CORS headers.
+ */
+ public function cors(ServerRequest $request): CorsBuilder
+ {
+ $origin = $request->getHeaderLine('Origin');
+ $ssl = $request->is('ssl');
+
+ return new CorsBuilder($this, $origin, $ssl);
+ }
+
+ /**
+ * Create a new instance that is based on a file.
+ *
+ * This method will augment both the body and a number of related headers.
+ *
+ * If `$_SERVER['HTTP_RANGE']` is set, a slice of the file will be
+ * returned instead of the entire file.
+ *
+ * ### Options keys
+ *
+ * - name: Alternate download name
+ * - download: If `true` sets download header and forces file to
+ * be downloaded rather than displayed inline.
+ *
+ * @param string $path Absolute path to file.
+ * @param array $options Options See above.
+ * @return static
+ * @throws \Cake\Http\Exception\NotFoundException
+ */
+ public function withFile(string $path, array $options = [])
+ {
+ $file = $this->validateFile($path);
+ $options += [
+ 'name' => null,
+ 'download' => null,
+ ];
+
+ $extension = strtolower($file->getExtension());
+ $mapped = $this->getMimeType($extension);
+ if ((!$extension || !$mapped) && $options['download'] === null) {
+ $options['download'] = true;
+ }
+
+ $new = clone $this;
+ if ($mapped) {
+ $new = $new->withType($extension);
+ }
+
+ $fileSize = $file->getSize();
+ if ($options['download']) {
+ $agent = (string)env('HTTP_USER_AGENT');
+
+ if ($agent && preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent)) {
+ $contentType = 'application/octet-stream';
+ } elseif ($agent && preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
+ $contentType = 'application/force-download';
+ }
+
+ if (isset($contentType)) {
+ $new = $new->withType($contentType);
+ }
+ $name = $options['name'] ?: $file->getFileName();
+ $new = $new->withDownload($name)
+ ->withHeader('Content-Transfer-Encoding', 'binary');
+ }
+
+ $new = $new->withHeader('Accept-Ranges', 'bytes');
+ $httpRange = (string)env('HTTP_RANGE');
+ if ($httpRange) {
+ $new->_fileRange($file, $httpRange);
+ } else {
+ $new = $new->withHeader('Content-Length', (string)$fileSize);
+ }
+ $new->_file = $file;
+ $new->stream = new Stream($file->getPathname(), 'rb');
+
+ return $new;
+ }
+
+ /**
+ * Convenience method to set a string into the response body
+ *
+ * @param string $string The string to be sent
+ * @return static
+ */
+ public function withStringBody(?string $string)
+ {
+ $new = clone $this;
+ $new->_createStream();
+ $new->stream->write((string)$string);
+
+ return $new;
+ }
+
+ /**
+ * Validate a file path is a valid response body.
+ *
+ * @param string $path The path to the file.
+ * @throws \Cake\Http\Exception\NotFoundException
+ * @return \SplFileInfo
+ */
+ protected function validateFile(string $path): SplFileInfo
+ {
+ if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) {
+ throw new NotFoundException(__d('cake', 'The requested file contains `..` and will not be read.'));
+ }
+
+ $file = new SplFileInfo($path);
+ if (!$file->isFile() || !$file->isReadable()) {
+ if (Configure::read('debug')) {
+ throw new NotFoundException(sprintf('The requested file %s was not found or not readable', $path));
+ }
+ throw new NotFoundException(__d('cake', 'The requested file was not found'));
+ }
+
+ return $file;
+ }
+
+ /**
+ * Get the current file if one exists.
+ *
+ * @return \SplFileInfo|null The file to use in the response or null
+ */
+ public function getFile(): ?SplFileInfo
+ {
+ return $this->_file;
+ }
+
+ /**
+ * Apply a file range to a file and set the end offset.
+ *
+ * If an invalid range is requested a 416 Status code will be used
+ * in the response.
+ *
+ * @param \SplFileInfo $file The file to set a range on.
+ * @param string $httpRange The range to use.
+ * @return void
+ */
+ protected function _fileRange(SplFileInfo $file, string $httpRange): void
+ {
+ $fileSize = $file->getSize();
+ $lastByte = $fileSize - 1;
+ $start = 0;
+ $end = $lastByte;
+
+ preg_match('/^bytes\s*=\s*(\d+)?\s*-\s*(\d+)?$/', $httpRange, $matches);
+ if ($matches) {
+ $start = $matches[1];
+ $end = $matches[2] ?? '';
+ }
+
+ if ($start === '') {
+ $start = $fileSize - (int)$end;
+ $end = $lastByte;
+ }
+ if ($end === '') {
+ $end = $lastByte;
+ }
+
+ if ($start > $end || $end > $lastByte || $start > $lastByte) {
+ $this->_setStatus(416);
+ $this->_setHeader('Content-Range', 'bytes 0-' . $lastByte . '/' . $fileSize);
+
+ return;
+ }
+
+ /** @psalm-suppress PossiblyInvalidOperand */
+ $this->_setHeader('Content-Length', (string)($end - $start + 1));
+ $this->_setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $fileSize);
+ $this->_setStatus(206);
+ $this->_fileRange = [$start, $end];
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'status' => $this->_status,
+ 'contentType' => $this->getType(),
+ 'headers' => $this->headers,
+ 'file' => $this->_file,
+ 'fileRange' => $this->_fileRange,
+ 'cookies' => $this->_cookies,
+ 'cacheDirectives' => $this->_cacheDirectives,
+ 'body' => (string)$this->getBody(),
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php b/app/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php
new file mode 100644
index 000000000..a0779bb8a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/ResponseEmitter.php
@@ -0,0 +1,296 @@
+maxBufferLength = $maxBufferLength;
+ }
+
+ /**
+ * Emit a response.
+ *
+ * Emits a response, including status line, headers, and the message body,
+ * according to the environment.
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response The response to emit.
+ * @return bool
+ */
+ public function emit(ResponseInterface $response): bool
+ {
+ $file = '';
+ $line = 0;
+ if (headers_sent($file, $line)) {
+ $message = "Unable to emit headers. Headers sent in file=$file line=$line";
+ trigger_error($message, E_USER_WARNING);
+ }
+
+ $this->emitStatusLine($response);
+ $this->emitHeaders($response);
+ $this->flush();
+
+ $range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
+ if (is_array($range)) {
+ $this->emitBodyRange($range, $response);
+ } else {
+ $this->emitBody($response);
+ }
+
+ if (function_exists('fastcgi_finish_request')) {
+ fastcgi_finish_request();
+ }
+
+ return true;
+ }
+
+ /**
+ * Emit the message body.
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response The response to emit
+ * @return void
+ */
+ protected function emitBody(ResponseInterface $response): void
+ {
+ if (in_array($response->getStatusCode(), [204, 304], true)) {
+ return;
+ }
+ $body = $response->getBody();
+
+ if (!$body->isSeekable()) {
+ echo $body;
+
+ return;
+ }
+
+ $body->rewind();
+ while (!$body->eof()) {
+ echo $body->read($this->maxBufferLength);
+ }
+ }
+
+ /**
+ * Emit a range of the message body.
+ *
+ * @param array $range The range data to emit
+ * @param \Psr\Http\Message\ResponseInterface $response The response to emit
+ * @return void
+ */
+ protected function emitBodyRange(array $range, ResponseInterface $response): void
+ {
+ [, $first, $last] = $range;
+
+ $body = $response->getBody();
+
+ if (!$body->isSeekable()) {
+ $contents = $body->getContents();
+ echo substr($contents, $first, $last - $first + 1);
+
+ return;
+ }
+
+ $body = new RelativeStream($body, $first);
+ $body->rewind();
+ $pos = 0;
+ $length = $last - $first + 1;
+ while (!$body->eof() && $pos < $length) {
+ if ($pos + $this->maxBufferLength > $length) {
+ echo $body->read($length - $pos);
+ break;
+ }
+
+ echo $body->read($this->maxBufferLength);
+ $pos = $body->tell();
+ }
+ }
+
+ /**
+ * Emit the status line.
+ *
+ * Emits the status line using the protocol version and status code from
+ * the response; if a reason phrase is available, it, too, is emitted.
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response The response to emit
+ * @return void
+ */
+ protected function emitStatusLine(ResponseInterface $response): void
+ {
+ $reasonPhrase = $response->getReasonPhrase();
+ header(sprintf(
+ 'HTTP/%s %d%s',
+ $response->getProtocolVersion(),
+ $response->getStatusCode(),
+ ($reasonPhrase ? ' ' . $reasonPhrase : '')
+ ));
+ }
+
+ /**
+ * Emit response headers.
+ *
+ * Loops through each header, emitting each; if the header value
+ * is an array with multiple values, ensures that each is sent
+ * in such a way as to create aggregate headers (instead of replace
+ * the previous).
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response The response to emit
+ * @return void
+ */
+ protected function emitHeaders(ResponseInterface $response): void
+ {
+ $cookies = [];
+ if (method_exists($response, 'getCookieCollection')) {
+ $cookies = iterator_to_array($response->getCookieCollection());
+ }
+
+ foreach ($response->getHeaders() as $name => $values) {
+ if (strtolower($name) === 'set-cookie') {
+ $cookies = array_merge($cookies, $values);
+ continue;
+ }
+ $first = true;
+ foreach ($values as $value) {
+ header(sprintf(
+ '%s: %s',
+ $name,
+ $value
+ ), $first);
+ $first = false;
+ }
+ }
+
+ $this->emitCookies($cookies);
+ }
+
+ /**
+ * Emit cookies using setcookie()
+ *
+ * @param (string|\Cake\Http\Cookie\CookieInterface)[] $cookies An array of cookies.
+ * @return void
+ */
+ protected function emitCookies(array $cookies): void
+ {
+ foreach ($cookies as $cookie) {
+ $this->setCookie($cookie);
+ }
+ }
+
+ /**
+ * Helper methods to set cookie.
+ *
+ * @param string|\Cake\Http\Cookie\CookieInterface $cookie Cookie.
+ * @return bool
+ */
+ protected function setCookie($cookie): bool
+ {
+ if (is_string($cookie)) {
+ $cookie = Cookie::createFromHeaderString($cookie, ['path' => '']);
+ }
+
+ if (PHP_VERSION_ID >= 70300) {
+ return setcookie($cookie->getName(), $cookie->getScalarValue(), $cookie->getOptions());
+ }
+
+ $path = $cookie->getPath();
+ $sameSite = $cookie->getSameSite();
+ if ($sameSite !== null) {
+ // Temporary hack for PHP 7.2 to set "SameSite" attribute
+ // https://stackoverflow.com/questions/39750906/php-setcookie-samesite-strict
+ $path .= '; samesite=' . $sameSite;
+ }
+
+ return setcookie(
+ $cookie->getName(),
+ $cookie->getScalarValue(),
+ $cookie->getExpiresTimestamp() ?: 0,
+ $path,
+ $cookie->getDomain(),
+ $cookie->isSecure(),
+ $cookie->isHttpOnly()
+ );
+ }
+
+ /**
+ * Loops through the output buffer, flushing each, before emitting
+ * the response.
+ *
+ * @param int|null $maxBufferLevel Flush up to this buffer level.
+ * @return void
+ */
+ protected function flush(?int $maxBufferLevel = null): void
+ {
+ if ($maxBufferLevel === null) {
+ $maxBufferLevel = ob_get_level();
+ }
+
+ while (ob_get_level() > $maxBufferLevel) {
+ ob_end_flush();
+ }
+ }
+
+ /**
+ * Parse content-range header
+ * https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
+ *
+ * @param string $header The Content-Range header to parse.
+ * @return array|false [unit, first, last, length]; returns false if no
+ * content range or an invalid content range is provided
+ */
+ protected function parseContentRange(string $header)
+ {
+ if (preg_match('/(?P[\w]+)\s+(?P\d+)-(?P\d+)\/(?P\d+|\*)/', $header, $matches)) {
+ return [
+ $matches['unit'],
+ (int)$matches['first'],
+ (int)$matches['last'],
+ $matches['length'] === '*' ? '*' : (int)$matches['length'],
+ ];
+ }
+
+ return false;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Runner.php b/app/vendor/cakephp/cakephp/src/Http/Runner.php
new file mode 100644
index 000000000..f9ced44c3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Runner.php
@@ -0,0 +1,88 @@
+queue = $queue;
+ $this->queue->rewind();
+ $this->fallbackHandler = $fallbackHandler;
+
+ return $this->handle($request);
+ }
+
+ /**
+ * Handle incoming server request and return a response.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The server request
+ * @return \Psr\Http\Message\ResponseInterface An updated response
+ */
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ if ($this->queue->valid()) {
+ $middleware = $this->queue->current();
+ $this->queue->next();
+
+ return $middleware->process($request, $this);
+ }
+
+ if ($this->fallbackHandler) {
+ return $this->fallbackHandler->handle($request);
+ }
+
+ $response = new Response([
+ 'body' => 'Middleware queue was exhausted without returning a response '
+ . 'and no fallback request handler was set for Runner',
+ 'status' => 500,
+ ]);
+
+ return $response;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Server.php b/app/vendor/cakephp/cakephp/src/Http/Server.php
new file mode 100644
index 000000000..913fbe053
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Server.php
@@ -0,0 +1,174 @@
+app = $app;
+ $this->runner = $runner ?? new Runner();
+ }
+
+ /**
+ * Run the request/response through the Application and its middleware.
+ *
+ * This will invoke the following methods:
+ *
+ * - App->bootstrap() - Perform any bootstrapping logic for your application here.
+ * - App->middleware() - Attach any application middleware here.
+ * - Trigger the 'Server.buildMiddleware' event. You can use this to modify the
+ * from event listeners.
+ * - Run the middleware queue including the application.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface|null $request The request to use or null.
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue MiddlewareQueue or null.
+ * @return \Psr\Http\Message\ResponseInterface
+ * @throws \RuntimeException When the application does not make a response.
+ */
+ public function run(
+ ?ServerRequestInterface $request = null,
+ ?MiddlewareQueue $middlewareQueue = null
+ ): ResponseInterface {
+ $this->bootstrap();
+
+ $request = $request ?: ServerRequestFactory::fromGlobals();
+
+ $middleware = $this->app->middleware($middlewareQueue ?? new MiddlewareQueue());
+ if ($this->app instanceof PluginApplicationInterface) {
+ $middleware = $this->app->pluginMiddleware($middleware);
+ }
+
+ $this->dispatchEvent('Server.buildMiddleware', ['middleware' => $middleware]);
+
+ $response = $this->runner->run($middleware, $request, $this->app);
+
+ if ($request instanceof ServerRequest) {
+ $request->getSession()->close();
+ }
+
+ return $response;
+ }
+
+ /**
+ * Application bootstrap wrapper.
+ *
+ * Calls the application's `bootstrap()` hook. After the application the
+ * plugins are bootstrapped.
+ *
+ * @return void
+ */
+ protected function bootstrap(): void
+ {
+ $this->app->bootstrap();
+ if ($this->app instanceof PluginApplicationInterface) {
+ $this->app->pluginBootstrap();
+ }
+ }
+
+ /**
+ * Emit the response using the PHP SAPI.
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response The response to emit
+ * @param \Laminas\HttpHandlerRunner\Emitter\EmitterInterface|null $emitter The emitter to use.
+ * When null, a SAPI Stream Emitter will be used.
+ * @return void
+ */
+ public function emit(ResponseInterface $response, ?EmitterInterface $emitter = null): void
+ {
+ if (!$emitter) {
+ $emitter = new ResponseEmitter();
+ }
+ $emitter->emit($response);
+ }
+
+ /**
+ * Get the current application.
+ *
+ * @return \Cake\Core\HttpApplicationInterface The application that will be run.
+ */
+ public function getApp(): HttpApplicationInterface
+ {
+ return $this->app;
+ }
+
+ /**
+ * Get the application's event manager or the global one.
+ *
+ * @return \Cake\Event\EventManagerInterface
+ */
+ public function getEventManager(): EventManagerInterface
+ {
+ if ($this->app instanceof EventDispatcherInterface) {
+ return $this->app->getEventManager();
+ }
+
+ return EventManager::instance();
+ }
+
+ /**
+ * Set the application's event manager.
+ *
+ * If the application does not support events, an exception will be raised.
+ *
+ * @param \Cake\Event\EventManagerInterface $eventManager The event manager to set.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setEventManager(EventManagerInterface $eventManager)
+ {
+ if ($this->app instanceof EventDispatcherInterface) {
+ $this->app->setEventManager($eventManager);
+
+ return $this;
+ }
+
+ throw new InvalidArgumentException('Cannot set the event manager, the application does not support events.');
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/ServerRequest.php b/app/vendor/cakephp/cakephp/src/Http/ServerRequest.php
new file mode 100644
index 000000000..2be352824
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/ServerRequest.php
@@ -0,0 +1,1883 @@
+ null,
+ 'controller' => null,
+ 'action' => null,
+ '_ext' => null,
+ 'pass' => [],
+ ];
+
+ /**
+ * Array of POST data. Will contain form data as well as uploaded files.
+ * In PUT/PATCH/DELETE requests this property will contain the form-urlencoded
+ * data.
+ *
+ * @var array|object|null
+ */
+ protected $data = [];
+
+ /**
+ * Array of query string arguments
+ *
+ * @var array
+ */
+ protected $query = [];
+
+ /**
+ * Array of cookie data.
+ *
+ * @var array
+ */
+ protected $cookies = [];
+
+ /**
+ * Array of environment data.
+ *
+ * @var array
+ */
+ protected $_environment = [];
+
+ /**
+ * Base URL path.
+ *
+ * @var string
+ */
+ protected $base;
+
+ /**
+ * webroot path segment for the request.
+ *
+ * @var string
+ */
+ protected $webroot = '/';
+
+ /**
+ * Whether or not to trust HTTP_X headers set by most load balancers.
+ * Only set to true if your application runs behind load balancers/proxies
+ * that you control.
+ *
+ * @var bool
+ */
+ public $trustProxy = false;
+
+ /**
+ * Trusted proxies list
+ *
+ * @var string[]
+ */
+ protected $trustedProxies = [];
+
+ /**
+ * The built in detectors used with `is()` can be modified with `addDetector()`.
+ *
+ * There are several ways to specify a detector, see \Cake\Http\ServerRequest::addDetector() for the
+ * various formats and ways to define detectors.
+ *
+ * @var (array|callable)[]
+ */
+ protected static $_detectors = [
+ 'get' => ['env' => 'REQUEST_METHOD', 'value' => 'GET'],
+ 'post' => ['env' => 'REQUEST_METHOD', 'value' => 'POST'],
+ 'put' => ['env' => 'REQUEST_METHOD', 'value' => 'PUT'],
+ 'patch' => ['env' => 'REQUEST_METHOD', 'value' => 'PATCH'],
+ 'delete' => ['env' => 'REQUEST_METHOD', 'value' => 'DELETE'],
+ 'head' => ['env' => 'REQUEST_METHOD', 'value' => 'HEAD'],
+ 'options' => ['env' => 'REQUEST_METHOD', 'value' => 'OPTIONS'],
+ 'ssl' => ['env' => 'HTTPS', 'options' => [1, 'on']],
+ 'ajax' => ['env' => 'HTTP_X_REQUESTED_WITH', 'value' => 'XMLHttpRequest'],
+ 'json' => ['accept' => ['application/json'], 'param' => '_ext', 'value' => 'json'],
+ 'xml' => ['accept' => ['application/xml', 'text/xml'], 'param' => '_ext', 'value' => 'xml'],
+ ];
+
+ /**
+ * Instance cache for results of is(something) calls
+ *
+ * @var array
+ */
+ protected $_detectorCache = [];
+
+ /**
+ * Request body stream. Contains php://input unless `input` constructor option is used.
+ *
+ * @var \Psr\Http\Message\StreamInterface
+ */
+ protected $stream;
+
+ /**
+ * Uri instance
+ *
+ * @var \Psr\Http\Message\UriInterface
+ */
+ protected $uri;
+
+ /**
+ * Instance of a Session object relative to this request
+ *
+ * @var \Cake\Http\Session
+ */
+ protected $session;
+
+ /**
+ * Instance of a FlashMessage object relative to this request
+ *
+ * @var \Cake\Http\FlashMessage
+ */
+ protected $flash;
+
+ /**
+ * Store the additional attributes attached to the request.
+ *
+ * @var array
+ */
+ protected $attributes = [];
+
+ /**
+ * A list of propertes that emulated by the PSR7 attribute methods.
+ *
+ * @var array
+ */
+ protected $emulatedAttributes = ['session', 'flash', 'webroot', 'base', 'params', 'here'];
+
+ /**
+ * Array of Psr\Http\Message\UploadedFileInterface objects.
+ *
+ * @var array
+ */
+ protected $uploadedFiles = [];
+
+ /**
+ * The HTTP protocol version used.
+ *
+ * @var string|null
+ */
+ protected $protocol;
+
+ /**
+ * The request target if overridden
+ *
+ * @var string|null
+ */
+ protected $requestTarget;
+
+ /**
+ * Create a new request object.
+ *
+ * You can supply the data as either an array or as a string. If you use
+ * a string you can only supply the URL for the request. Using an array will
+ * let you provide the following keys:
+ *
+ * - `post` POST data or non query string data
+ * - `query` Additional data from the query string.
+ * - `files` Uploaded files in a normalized structure, with each leaf an instance of UploadedFileInterface.
+ * - `cookies` Cookies for this request.
+ * - `environment` $_SERVER and $_ENV data.
+ * - `url` The URL without the base path for the request.
+ * - `uri` The PSR7 UriInterface object. If null, one will be created from `url` or `environment`.
+ * - `base` The base URL for the request.
+ * - `webroot` The webroot directory for the request.
+ * - `input` The data that would come from php://input this is useful for simulating
+ * requests with put, patch or delete data.
+ * - `session` An instance of a Session object
+ *
+ * @param array $config An array of request data to create a request with.
+ */
+ public function __construct(array $config = [])
+ {
+ $config += [
+ 'params' => $this->params,
+ 'query' => [],
+ 'post' => [],
+ 'files' => [],
+ 'cookies' => [],
+ 'environment' => [],
+ 'url' => '',
+ 'uri' => null,
+ 'base' => '',
+ 'webroot' => '',
+ 'input' => null,
+ ];
+
+ $this->_setConfig($config);
+ }
+
+ /**
+ * Process the config/settings data into properties.
+ *
+ * @param array $config The config data to use.
+ * @return void
+ */
+ protected function _setConfig(array $config): void
+ {
+ if (empty($config['session'])) {
+ $config['session'] = new Session([
+ 'cookiePath' => $config['base'],
+ ]);
+ }
+
+ if (empty($config['environment']['REQUEST_METHOD'])) {
+ $config['environment']['REQUEST_METHOD'] = 'GET';
+ }
+
+ $this->cookies = $config['cookies'];
+
+ if (isset($config['uri'])) {
+ if (!$config['uri'] instanceof UriInterface) {
+ throw new CakeException('The `uri` key must be an instance of ' . UriInterface::class);
+ }
+ $uri = $config['uri'];
+ } else {
+ if ($config['url'] !== '') {
+ $config = $this->processUrlOption($config);
+ }
+ $uri = ServerRequestFactory::createUri($config['environment']);
+ }
+
+ $this->_environment = $config['environment'];
+
+ $this->uri = $uri;
+ $this->base = $config['base'];
+ $this->webroot = $config['webroot'];
+
+ if (isset($config['input'])) {
+ $stream = new Stream('php://memory', 'rw');
+ $stream->write($config['input']);
+ $stream->rewind();
+ } else {
+ $stream = new PhpInputStream();
+ }
+ $this->stream = $stream;
+
+ $this->data = $config['post'];
+ $this->uploadedFiles = $config['files'];
+ $this->query = $config['query'];
+ $this->params = $config['params'];
+ $this->session = $config['session'];
+ $this->flash = new FlashMessage($this->session);
+ }
+
+ /**
+ * Set environment vars based on `url` option to facilitate UriInterface instance generation.
+ *
+ * `query` option is also updated based on URL's querystring.
+ *
+ * @param array $config Config array.
+ * @return array Update config.
+ */
+ protected function processUrlOption(array $config): array
+ {
+ if ($config['url'][0] !== '/') {
+ $config['url'] = '/' . $config['url'];
+ }
+
+ if (strpos($config['url'], '?') !== false) {
+ [$config['url'], $config['environment']['QUERY_STRING']] = explode('?', $config['url']);
+
+ parse_str($config['environment']['QUERY_STRING'], $queryArgs);
+ $config['query'] += $queryArgs;
+ }
+
+ $config['environment']['REQUEST_URI'] = $config['url'];
+
+ return $config;
+ }
+
+ /**
+ * Get the content type used in this request.
+ *
+ * @return string|null
+ */
+ public function contentType(): ?string
+ {
+ $type = $this->getEnv('CONTENT_TYPE');
+ if ($type) {
+ return $type;
+ }
+
+ return $this->getEnv('HTTP_CONTENT_TYPE');
+ }
+
+ /**
+ * Returns the instance of the Session object for this request
+ *
+ * @return \Cake\Http\Session
+ */
+ public function getSession(): Session
+ {
+ return $this->session;
+ }
+
+ /**
+ * Returns the instance of the FlashMessage object for this request
+ *
+ * @return \Cake\Http\FlashMessage
+ */
+ public function getFlash(): FlashMessage
+ {
+ return $this->flash;
+ }
+
+ /**
+ * Get the IP the client is using, or says they are using.
+ *
+ * @return string The client IP.
+ */
+ public function clientIp(): string
+ {
+ if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_FOR')) {
+ $addresses = array_map('trim', explode(',', (string)$this->getEnv('HTTP_X_FORWARDED_FOR')));
+ $trusted = (count($this->trustedProxies) > 0);
+ $n = count($addresses);
+
+ if ($trusted) {
+ $trusted = array_diff($addresses, $this->trustedProxies);
+ $trusted = (count($trusted) === 1);
+ }
+
+ if ($trusted) {
+ return $addresses[0];
+ }
+
+ return $addresses[$n - 1];
+ }
+
+ if ($this->trustProxy && $this->getEnv('HTTP_X_REAL_IP')) {
+ $ipaddr = $this->getEnv('HTTP_X_REAL_IP');
+ } elseif ($this->trustProxy && $this->getEnv('HTTP_CLIENT_IP')) {
+ $ipaddr = $this->getEnv('HTTP_CLIENT_IP');
+ } else {
+ $ipaddr = $this->getEnv('REMOTE_ADDR');
+ }
+
+ return trim((string)$ipaddr);
+ }
+
+ /**
+ * register trusted proxies
+ *
+ * @param string[] $proxies ips list of trusted proxies
+ * @return void
+ */
+ public function setTrustedProxies(array $proxies): void
+ {
+ $this->trustedProxies = $proxies;
+ $this->trustProxy = true;
+ }
+
+ /**
+ * Get trusted proxies
+ *
+ * @return string[]
+ */
+ public function getTrustedProxies(): array
+ {
+ return $this->trustedProxies;
+ }
+
+ /**
+ * Returns the referer that referred this request.
+ *
+ * @param bool $local Attempt to return a local address.
+ * Local addresses do not contain hostnames.
+ * @return string|null The referring address for this request or null.
+ */
+ public function referer(bool $local = true): ?string
+ {
+ $ref = $this->getEnv('HTTP_REFERER');
+
+ $base = Configure::read('App.fullBaseUrl') . $this->webroot;
+ if (!empty($ref) && !empty($base)) {
+ if ($local && strpos($ref, $base) === 0) {
+ $ref = substr($ref, strlen($base));
+ if (!strlen($ref) || strpos($ref, '//') === 0) {
+ $ref = '/';
+ }
+ if ($ref[0] !== '/') {
+ $ref = '/' . $ref;
+ }
+
+ return $ref;
+ }
+ if (!$local) {
+ return $ref;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Missing method handler, handles wrapping older style isAjax() type methods
+ *
+ * @param string $name The method called
+ * @param array $params Array of parameters for the method call
+ * @return mixed
+ * @throws \BadMethodCallException when an invalid method is called.
+ */
+ public function __call(string $name, array $params)
+ {
+ if (strpos($name, 'is') === 0) {
+ $type = strtolower(substr($name, 2));
+
+ array_unshift($params, $type);
+
+ return $this->is(...$params);
+ }
+ throw new BadMethodCallException(sprintf('Method "%s()" does not exist', $name));
+ }
+
+ /**
+ * Check whether or not a Request is a certain type.
+ *
+ * Uses the built in detection rules as well as additional rules
+ * defined with Cake\Http\ServerRequest::addDetector(). Any detector can be called
+ * as `is($type)` or `is$Type()`.
+ *
+ * @param string|string[] $type The type of request you want to check. If an array
+ * this method will return true if the request matches any type.
+ * @param mixed ...$args List of arguments
+ * @return bool Whether or not the request is the type you are checking.
+ */
+ public function is($type, ...$args): bool
+ {
+ if (is_array($type)) {
+ foreach ($type as $_type) {
+ if ($this->is($_type)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ $type = strtolower($type);
+ if (!isset(static::$_detectors[$type])) {
+ return false;
+ }
+ if ($args) {
+ return $this->_is($type, $args);
+ }
+ if (!isset($this->_detectorCache[$type])) {
+ $this->_detectorCache[$type] = $this->_is($type, $args);
+ }
+
+ return $this->_detectorCache[$type];
+ }
+
+ /**
+ * Clears the instance detector cache, used by the is() function
+ *
+ * @return void
+ */
+ public function clearDetectorCache(): void
+ {
+ $this->_detectorCache = [];
+ }
+
+ /**
+ * Worker for the public is() function
+ *
+ * @param string $type The type of request you want to check.
+ * @param array $args Array of custom detector arguments.
+ * @return bool Whether or not the request is the type you are checking.
+ */
+ protected function _is(string $type, array $args): bool
+ {
+ $detect = static::$_detectors[$type];
+ if (is_callable($detect)) {
+ array_unshift($args, $this);
+
+ return $detect(...$args);
+ }
+ if (isset($detect['env']) && $this->_environmentDetector($detect)) {
+ return true;
+ }
+ if (isset($detect['header']) && $this->_headerDetector($detect)) {
+ return true;
+ }
+ if (isset($detect['accept']) && $this->_acceptHeaderDetector($detect)) {
+ return true;
+ }
+ if (isset($detect['param']) && $this->_paramDetector($detect)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Detects if a specific accept header is present.
+ *
+ * @param array $detect Detector options array.
+ * @return bool Whether or not the request is the type you are checking.
+ */
+ protected function _acceptHeaderDetector(array $detect): bool
+ {
+ $acceptHeaders = explode(',', (string)$this->getEnv('HTTP_ACCEPT'));
+ foreach ($detect['accept'] as $header) {
+ if (in_array($header, $acceptHeaders, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Detects if a specific header is present.
+ *
+ * @param array $detect Detector options array.
+ * @return bool Whether or not the request is the type you are checking.
+ */
+ protected function _headerDetector(array $detect): bool
+ {
+ foreach ($detect['header'] as $header => $value) {
+ $header = $this->getEnv('http_' . $header);
+ if ($header !== null) {
+ if (!is_string($value) && !is_bool($value) && is_callable($value)) {
+ return $value($header);
+ }
+
+ return $header === $value;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Detects if a specific request parameter is present.
+ *
+ * @param array $detect Detector options array.
+ * @return bool Whether or not the request is the type you are checking.
+ */
+ protected function _paramDetector(array $detect): bool
+ {
+ $key = $detect['param'];
+ if (isset($detect['value'])) {
+ $value = $detect['value'];
+
+ return isset($this->params[$key]) ? $this->params[$key] == $value : false;
+ }
+ if (isset($detect['options'])) {
+ return isset($this->params[$key]) ? in_array($this->params[$key], $detect['options']) : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Detects if a specific environment variable is present.
+ *
+ * @param array $detect Detector options array.
+ * @return bool Whether or not the request is the type you are checking.
+ */
+ protected function _environmentDetector(array $detect): bool
+ {
+ if (isset($detect['env'])) {
+ if (isset($detect['value'])) {
+ return $this->getEnv($detect['env']) == $detect['value'];
+ }
+ if (isset($detect['pattern'])) {
+ return (bool)preg_match($detect['pattern'], (string)$this->getEnv($detect['env']));
+ }
+ if (isset($detect['options'])) {
+ $pattern = '/' . implode('|', $detect['options']) . '/i';
+
+ return (bool)preg_match($pattern, (string)$this->getEnv($detect['env']));
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check that a request matches all the given types.
+ *
+ * Allows you to test multiple types and union the results.
+ * See Request::is() for how to add additional types and the
+ * built-in types.
+ *
+ * @param string[] $types The types to check.
+ * @return bool Success.
+ * @see \Cake\Http\ServerRequest::is()
+ */
+ public function isAll(array $types): bool
+ {
+ foreach ($types as $type) {
+ if (!$this->is($type)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a new detector to the list of detectors that a request can use.
+ * There are several different types of detectors that can be set.
+ *
+ * ### Callback comparison
+ *
+ * Callback detectors allow you to provide a callable to handle the check.
+ * The callback will receive the request object as its only parameter.
+ *
+ * ```
+ * addDetector('custom', function ($request) { //Return a boolean });
+ * ```
+ *
+ * ### Environment value comparison
+ *
+ * An environment value comparison, compares a value fetched from `env()` to a known value
+ * the environment value is equality checked against the provided value.
+ *
+ * ```
+ * addDetector('post', ['env' => 'REQUEST_METHOD', 'value' => 'POST']);
+ * ```
+ *
+ * ### Request parameter comparison
+ *
+ * Allows for custom detectors on the request parameters.
+ *
+ * ```
+ * addDetector('admin', ['param' => 'prefix', 'value' => 'admin']);
+ * ```
+ *
+ * ### Accept comparison
+ *
+ * Allows for detector to compare against Accept header value.
+ *
+ * ```
+ * addDetector('csv', ['accept' => 'text/csv']);
+ * ```
+ *
+ * ### Header comparison
+ *
+ * Allows for one or more headers to be compared.
+ *
+ * ```
+ * addDetector('fancy', ['header' => ['X-Fancy' => 1]);
+ * ```
+ *
+ * The `param`, `env` and comparison types allow the following
+ * value comparison options:
+ *
+ * ### Pattern value comparison
+ *
+ * Pattern value comparison allows you to compare a value fetched from `env()` to a regular expression.
+ *
+ * ```
+ * addDetector('iphone', ['env' => 'HTTP_USER_AGENT', 'pattern' => '/iPhone/i']);
+ * ```
+ *
+ * ### Option based comparison
+ *
+ * Option based comparisons use a list of options to create a regular expression. Subsequent calls
+ * to add an already defined options detector will merge the options.
+ *
+ * ```
+ * addDetector('mobile', ['env' => 'HTTP_USER_AGENT', 'options' => ['Fennec']]);
+ * ```
+ *
+ * You can also make compare against multiple values
+ * using the `options` key. This is useful when you want to check
+ * if a request value is in a list of options.
+ *
+ * `addDetector('extension', ['param' => '_ext', 'options' => ['pdf', 'csv']]`
+ *
+ * @param string $name The name of the detector.
+ * @param callable|array $callable A callable or options array for the detector definition.
+ * @return void
+ */
+ public static function addDetector(string $name, $callable): void
+ {
+ $name = strtolower($name);
+ if (is_callable($callable)) {
+ static::$_detectors[$name] = $callable;
+
+ return;
+ }
+ if (isset(static::$_detectors[$name], $callable['options'])) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $callable = Hash::merge(static::$_detectors[$name], $callable);
+ }
+ static::$_detectors[$name] = $callable;
+ }
+
+ /**
+ * Normalize a header name into the SERVER version.
+ *
+ * @param string $name The header name.
+ * @return string The normalized header name.
+ */
+ protected function normalizeHeaderName(string $name): string
+ {
+ $name = str_replace('-', '_', strtoupper($name));
+ if (!in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'], true)) {
+ $name = 'HTTP_' . $name;
+ }
+
+ return $name;
+ }
+
+ /**
+ * Get all headers in the request.
+ *
+ * Returns an associative array where the header names are
+ * the keys and the values are a list of header values.
+ *
+ * While header names are not case-sensitive, getHeaders() will normalize
+ * the headers.
+ *
+ * @return string[][] An associative array of headers and their values.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function getHeaders(): array
+ {
+ $headers = [];
+ foreach ($this->_environment as $key => $value) {
+ $name = null;
+ if (strpos($key, 'HTTP_') === 0) {
+ $name = substr($key, 5);
+ }
+ if (strpos($key, 'CONTENT_') === 0) {
+ $name = $key;
+ }
+ if ($name !== null) {
+ $name = str_replace('_', ' ', strtolower($name));
+ $name = str_replace(' ', '-', ucwords($name));
+ $headers[$name] = (array)$value;
+ }
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Check if a header is set in the request.
+ *
+ * @param string $name The header you want to get (case-insensitive)
+ * @return bool Whether or not the header is defined.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function hasHeader($name): bool
+ {
+ $name = $this->normalizeHeaderName($name);
+
+ return isset($this->_environment[$name]);
+ }
+
+ /**
+ * Get a single header from the request.
+ *
+ * Return the header value as an array. If the header
+ * is not present an empty array will be returned.
+ *
+ * @param string $name The header you want to get (case-insensitive)
+ * @return string[] An associative array of headers and their values.
+ * If the header doesn't exist, an empty array will be returned.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function getHeader($name): array
+ {
+ $name = $this->normalizeHeaderName($name);
+ if (isset($this->_environment[$name])) {
+ return (array)$this->_environment[$name];
+ }
+
+ return [];
+ }
+
+ /**
+ * Get a single header as a string from the request.
+ *
+ * @param string $name The header you want to get (case-insensitive)
+ * @return string Header values collapsed into a comma separated string.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function getHeaderLine($name): string
+ {
+ $value = $this->getHeader($name);
+
+ return implode(', ', $value);
+ }
+
+ /**
+ * Get a modified request with the provided header.
+ *
+ * @param string $name The header name.
+ * @param string|array $value The header value
+ * @return static
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function withHeader($name, $value)
+ {
+ $new = clone $this;
+ $name = $this->normalizeHeaderName($name);
+ $new->_environment[$name] = $value;
+
+ return $new;
+ }
+
+ /**
+ * Get a modified request with the provided header.
+ *
+ * Existing header values will be retained. The provided value
+ * will be appended into the existing values.
+ *
+ * @param string $name The header name.
+ * @param string|array $value The header value
+ * @return static
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function withAddedHeader($name, $value)
+ {
+ $new = clone $this;
+ $name = $this->normalizeHeaderName($name);
+ $existing = [];
+ if (isset($new->_environment[$name])) {
+ $existing = (array)$new->_environment[$name];
+ }
+ $existing = array_merge($existing, (array)$value);
+ $new->_environment[$name] = $existing;
+
+ return $new;
+ }
+
+ /**
+ * Get a modified request without a provided header.
+ *
+ * @param string $name The header name to remove.
+ * @return static
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function withoutHeader($name)
+ {
+ $new = clone $this;
+ $name = $this->normalizeHeaderName($name);
+ unset($new->_environment[$name]);
+
+ return $new;
+ }
+
+ /**
+ * Get the HTTP method used for this request.
+ * There are a few ways to specify a method.
+ *
+ * - If your client supports it you can use native HTTP methods.
+ * - You can set the HTTP-X-Method-Override header.
+ * - You can submit an input with the name `_method`
+ *
+ * Any of these 3 approaches can be used to set the HTTP method used
+ * by CakePHP internally, and will effect the result of this method.
+ *
+ * @return string The name of the HTTP method used.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function getMethod(): string
+ {
+ return (string)$this->getEnv('REQUEST_METHOD');
+ }
+
+ /**
+ * Update the request method and get a new instance.
+ *
+ * @param string $method The HTTP method to use.
+ * @return static A new instance with the updated method.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function withMethod($method)
+ {
+ $new = clone $this;
+
+ if (
+ !is_string($method) ||
+ !preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)
+ ) {
+ throw new InvalidArgumentException(sprintf(
+ 'Unsupported HTTP method "%s" provided',
+ $method
+ ));
+ }
+ $new->_environment['REQUEST_METHOD'] = $method;
+
+ return $new;
+ }
+
+ /**
+ * Get all the server environment parameters.
+ *
+ * Read all of the 'environment' or 'server' data that was
+ * used to create this request.
+ *
+ * @return array
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function getServerParams(): array
+ {
+ return $this->_environment;
+ }
+
+ /**
+ * Get all the query parameters in accordance to the PSR-7 specifications. To read specific query values
+ * use the alternative getQuery() method.
+ *
+ * @return array
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function getQueryParams(): array
+ {
+ return $this->query;
+ }
+
+ /**
+ * Update the query string data and get a new instance.
+ *
+ * @param array $query The query string data to use
+ * @return static A new instance with the updated query string data.
+ * @link http://www.php-fig.org/psr/psr-7/ This method is part of the PSR-7 server request interface.
+ */
+ public function withQueryParams(array $query)
+ {
+ $new = clone $this;
+ $new->query = $query;
+
+ return $new;
+ }
+
+ /**
+ * Get the host that the request was handled on.
+ *
+ * @return string|null
+ */
+ public function host(): ?string
+ {
+ if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_HOST')) {
+ return $this->getEnv('HTTP_X_FORWARDED_HOST');
+ }
+
+ return $this->getEnv('HTTP_HOST');
+ }
+
+ /**
+ * Get the port the request was handled on.
+ *
+ * @return string|null
+ */
+ public function port(): ?string
+ {
+ if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PORT')) {
+ return $this->getEnv('HTTP_X_FORWARDED_PORT');
+ }
+
+ return $this->getEnv('SERVER_PORT');
+ }
+
+ /**
+ * Get the current url scheme used for the request.
+ *
+ * e.g. 'http', or 'https'
+ *
+ * @return string|null The scheme used for the request.
+ */
+ public function scheme(): ?string
+ {
+ if ($this->trustProxy && $this->getEnv('HTTP_X_FORWARDED_PROTO')) {
+ return $this->getEnv('HTTP_X_FORWARDED_PROTO');
+ }
+
+ return $this->getEnv('HTTPS') ? 'https' : 'http';
+ }
+
+ /**
+ * Get the domain name and include $tldLength segments of the tld.
+ *
+ * @param int $tldLength Number of segments your tld contains. For example: `example.com` contains 1 tld.
+ * While `example.co.uk` contains 2.
+ * @return string Domain name without subdomains.
+ */
+ public function domain(int $tldLength = 1): string
+ {
+ $host = $this->host();
+ if (empty($host)) {
+ return '';
+ }
+
+ $segments = explode('.', $host);
+ $domain = array_slice($segments, -1 * ($tldLength + 1));
+
+ return implode('.', $domain);
+ }
+
+ /**
+ * Get the subdomains for a host.
+ *
+ * @param int $tldLength Number of segments your tld contains. For example: `example.com` contains 1 tld.
+ * While `example.co.uk` contains 2.
+ * @return string[] An array of subdomains.
+ */
+ public function subdomains(int $tldLength = 1): array
+ {
+ $host = $this->host();
+ if (empty($host)) {
+ return [];
+ }
+
+ $segments = explode('.', $host);
+
+ return array_slice($segments, 0, -1 * ($tldLength + 1));
+ }
+
+ /**
+ * Find out which content types the client accepts or check if they accept a
+ * particular type of content.
+ *
+ * #### Get all types:
+ *
+ * ```
+ * $this->request->accepts();
+ * ```
+ *
+ * #### Check for a single type:
+ *
+ * ```
+ * $this->request->accepts('application/json');
+ * ```
+ *
+ * This method will order the returned content types by the preference values indicated
+ * by the client.
+ *
+ * @param string|null $type The content type to check for. Leave null to get all types a client accepts.
+ * @return array|bool Either an array of all the types the client accepts or a boolean if they accept the
+ * provided type.
+ */
+ public function accepts(?string $type = null)
+ {
+ $raw = $this->parseAccept();
+ $accept = [];
+ foreach ($raw as $types) {
+ $accept = array_merge($accept, $types);
+ }
+ if ($type === null) {
+ return $accept;
+ }
+
+ return in_array($type, $accept, true);
+ }
+
+ /**
+ * Parse the HTTP_ACCEPT header and return a sorted array with content types
+ * as the keys, and pref values as the values.
+ *
+ * Generally you want to use Cake\Http\ServerRequest::accept() to get a simple list
+ * of the accepted content types.
+ *
+ * @return array An array of prefValue => [content/types]
+ */
+ public function parseAccept(): array
+ {
+ return $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept'));
+ }
+
+ /**
+ * Get the languages accepted by the client, or check if a specific language is accepted.
+ *
+ * Get the list of accepted languages:
+ *
+ * ``` \Cake\Http\ServerRequest::acceptLanguage(); ```
+ *
+ * Check if a specific language is accepted:
+ *
+ * ``` \Cake\Http\ServerRequest::acceptLanguage('es-es'); ```
+ *
+ * @param string|null $language The language to test.
+ * @return array|bool If a $language is provided, a boolean. Otherwise the array of accepted languages.
+ */
+ public function acceptLanguage(?string $language = null)
+ {
+ $raw = $this->_parseAcceptWithQualifier($this->getHeaderLine('Accept-Language'));
+ $accept = [];
+ foreach ($raw as $languages) {
+ foreach ($languages as &$lang) {
+ if (strpos($lang, '_')) {
+ $lang = str_replace('_', '-', $lang);
+ }
+ $lang = strtolower($lang);
+ }
+ $accept = array_merge($accept, $languages);
+ }
+ if ($language === null) {
+ return $accept;
+ }
+
+ return in_array(strtolower($language), $accept, true);
+ }
+
+ /**
+ * Parse Accept* headers with qualifier options.
+ *
+ * Only qualifiers will be extracted, any other accept extensions will be
+ * discarded as they are not frequently used.
+ *
+ * @param string $header Header to parse.
+ * @return array
+ */
+ protected function _parseAcceptWithQualifier(string $header): array
+ {
+ $accept = [];
+ $headers = explode(',', $header);
+ foreach (array_filter($headers) as $value) {
+ $prefValue = '1.0';
+ $value = trim($value);
+
+ $semiPos = strpos($value, ';');
+ if ($semiPos !== false) {
+ $params = explode(';', $value);
+ $value = trim($params[0]);
+ foreach ($params as $param) {
+ $qPos = strpos($param, 'q=');
+ if ($qPos !== false) {
+ $prefValue = substr($param, $qPos + 2);
+ }
+ }
+ }
+
+ if (!isset($accept[$prefValue])) {
+ $accept[$prefValue] = [];
+ }
+ if ($prefValue) {
+ $accept[$prefValue][] = $value;
+ }
+ }
+ krsort($accept);
+
+ return $accept;
+ }
+
+ /**
+ * Read a specific query value or dotted path.
+ *
+ * Developers are encouraged to use getQueryParams() if they need the whole query array,
+ * as it is PSR-7 compliant, and this method is not. Using Hash::get() you can also get single params.
+ *
+ * ### PSR-7 Alternative
+ *
+ * ```
+ * $value = Hash::get($request->getQueryParams(), 'Post.id');
+ * ```
+ *
+ * @param string|null $name The name or dotted path to the query param or null to read all.
+ * @param mixed $default The default value if the named parameter is not set, and $name is not null.
+ * @return array|string|null Query data.
+ * @see ServerRequest::getQueryParams()
+ */
+ public function getQuery(?string $name = null, $default = null)
+ {
+ if ($name === null) {
+ return $this->query;
+ }
+
+ return Hash::get($this->query, $name, $default);
+ }
+
+ /**
+ * Provides a safe accessor for request data. Allows
+ * you to use Hash::get() compatible paths.
+ *
+ * ### Reading values.
+ *
+ * ```
+ * // get all data
+ * $request->getData();
+ *
+ * // Read a specific field.
+ * $request->getData('Post.title');
+ *
+ * // With a default value.
+ * $request->getData('Post.not there', 'default value');
+ * ```
+ *
+ * When reading values you will get `null` for keys/values that do not exist.
+ *
+ * Developers are encouraged to use getParsedBody() if they need the whole data array,
+ * as it is PSR-7 compliant, and this method is not. Using Hash::get() you can also get single params.
+ *
+ * ### PSR-7 Alternative
+ *
+ * ```
+ * $value = Hash::get($request->getParsedBody(), 'Post.id');
+ * ```
+ *
+ * @param string|null $name Dot separated name of the value to read. Or null to read all data.
+ * @param mixed $default The default data.
+ * @return mixed The value being read.
+ */
+ public function getData(?string $name = null, $default = null)
+ {
+ if ($name === null) {
+ return $this->data;
+ }
+ if (!is_array($this->data) && $name) {
+ return $default;
+ }
+
+ /** @psalm-suppress PossiblyNullArgument */
+ return Hash::get($this->data, $name, $default);
+ }
+
+ /**
+ * Read data from `php://input`. Useful when interacting with XML or JSON
+ * request body content.
+ *
+ * Getting input with a decoding function:
+ *
+ * ```
+ * $this->request->input('json_decode');
+ * ```
+ *
+ * Getting input using a decoding function, and additional params:
+ *
+ * ```
+ * $this->request->input('Xml::build', ['return' => 'DOMDocument']);
+ * ```
+ *
+ * Any additional parameters are applied to the callback in the order they are given.
+ *
+ * @deprecated 4.1.0 Use `(string)$request->getBody()` to get the raw PHP input
+ * as string; use `BodyParserMiddleware` to parse the request body so that it's
+ * available as array/object through `$request->getParsedBody()`.
+ * @param callable|null $callback A decoding callback that will convert the string data to another
+ * representation. Leave empty to access the raw input data. You can also
+ * supply additional parameters for the decoding callback using var args, see above.
+ * @param mixed ...$args The additional arguments
+ * @return mixed The decoded/processed request data.
+ */
+ public function input(?callable $callback = null, ...$args)
+ {
+ deprecationWarning(
+ 'Use `(string)$request->getBody()` to get the raw PHP input as string; '
+ . 'use `BodyParserMiddleware` to parse the request body so that it\'s available as array/object '
+ . 'through $request->getParsedBody()'
+ );
+ $this->stream->rewind();
+ $input = $this->stream->getContents();
+ if ($callback) {
+ array_unshift($args, $input);
+
+ return $callback(...$args);
+ }
+
+ return $input;
+ }
+
+ /**
+ * Read cookie data from the request's cookie data.
+ *
+ * @param string $key The key or dotted path you want to read.
+ * @param string|array|null $default The default value if the cookie is not set.
+ * @return string|array|null Either the cookie value, or null if the value doesn't exist.
+ */
+ public function getCookie(string $key, $default = null)
+ {
+ return Hash::get($this->cookies, $key, $default);
+ }
+
+ /**
+ * Get a cookie collection based on the request's cookies
+ *
+ * The CookieCollection lets you interact with request cookies using
+ * `\Cake\Http\Cookie\Cookie` objects and can make converting request cookies
+ * into response cookies easier.
+ *
+ * This method will create a new cookie collection each time it is called.
+ * This is an optimization that allows fewer objects to be allocated until
+ * the more complex CookieCollection is needed. In general you should prefer
+ * `getCookie()` and `getCookieParams()` over this method. Using a CookieCollection
+ * is ideal if your cookies contain complex JSON encoded data.
+ *
+ * @return \Cake\Http\Cookie\CookieCollection
+ */
+ public function getCookieCollection(): CookieCollection
+ {
+ return CookieCollection::createFromServerRequest($this);
+ }
+
+ /**
+ * Replace the cookies in the request with those contained in
+ * the provided CookieCollection.
+ *
+ * @param \Cake\Http\Cookie\CookieCollection $cookies The cookie collection
+ * @return static
+ */
+ public function withCookieCollection(CookieCollection $cookies)
+ {
+ $new = clone $this;
+ $values = [];
+ foreach ($cookies as $cookie) {
+ $values[$cookie->getName()] = $cookie->getValue();
+ }
+ $new->cookies = $values;
+
+ return $new;
+ }
+
+ /**
+ * Get all the cookie data from the request.
+ *
+ * @return array An array of cookie data.
+ */
+ public function getCookieParams(): array
+ {
+ return $this->cookies;
+ }
+
+ /**
+ * Replace the cookies and get a new request instance.
+ *
+ * @param array $cookies The new cookie data to use.
+ * @return static
+ */
+ public function withCookieParams(array $cookies)
+ {
+ $new = clone $this;
+ $new->cookies = $cookies;
+
+ return $new;
+ }
+
+ /**
+ * Get the parsed request body data.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, this will be the
+ * post data. For other content types, it may be the deserialized request
+ * body.
+ *
+ * @return array|object|null The deserialized body parameters, if any.
+ * These will typically be an array.
+ */
+ public function getParsedBody()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Update the parsed body and get a new instance.
+ *
+ * @param array|object|null $data The deserialized body data. This will
+ * typically be in an array or object.
+ * @return static
+ */
+ public function withParsedBody($data)
+ {
+ $new = clone $this;
+ $new->data = $data;
+
+ return $new;
+ }
+
+ /**
+ * Retrieves the HTTP protocol version as a string.
+ *
+ * @return string HTTP protocol version.
+ */
+ public function getProtocolVersion(): string
+ {
+ if ($this->protocol) {
+ return $this->protocol;
+ }
+
+ // Lazily populate this data as it is generally not used.
+ preg_match('/^HTTP\/([\d.]+)$/', (string)$this->getEnv('SERVER_PROTOCOL'), $match);
+ $protocol = '1.1';
+ if (isset($match[1])) {
+ $protocol = $match[1];
+ }
+ $this->protocol = $protocol;
+
+ return $this->protocol;
+ }
+
+ /**
+ * Return an instance with the specified HTTP protocol version.
+ *
+ * The version string MUST contain only the HTTP version number (e.g.,
+ * "1.1", "1.0").
+ *
+ * @param string $version HTTP protocol version
+ * @return static
+ */
+ public function withProtocolVersion($version)
+ {
+ if (!preg_match('/^(1\.[01]|2)$/', $version)) {
+ throw new InvalidArgumentException("Unsupported protocol version '{$version}' provided");
+ }
+ $new = clone $this;
+ $new->protocol = $version;
+
+ return $new;
+ }
+
+ /**
+ * Get a value from the request's environment data.
+ * Fallback to using env() if the key is not set in the $environment property.
+ *
+ * @param string $key The key you want to read from.
+ * @param string|null $default Default value when trying to retrieve an environment
+ * variable's value that does not exist.
+ * @return string|null Either the environment value, or null if the value doesn't exist.
+ */
+ public function getEnv(string $key, ?string $default = null): ?string
+ {
+ $key = strtoupper($key);
+ if (!array_key_exists($key, $this->_environment)) {
+ $this->_environment[$key] = env($key);
+ }
+
+ return $this->_environment[$key] !== null ? (string)$this->_environment[$key] : $default;
+ }
+
+ /**
+ * Update the request with a new environment data element.
+ *
+ * Returns an updated request object. This method returns
+ * a *new* request object and does not mutate the request in-place.
+ *
+ * @param string $key The key you want to write to.
+ * @param string $value Value to set
+ * @return static
+ */
+ public function withEnv(string $key, string $value)
+ {
+ $new = clone $this;
+ $new->_environment[$key] = $value;
+ $new->clearDetectorCache();
+
+ return $new;
+ }
+
+ /**
+ * Allow only certain HTTP request methods, if the request method does not match
+ * a 405 error will be shown and the required "Allow" response header will be set.
+ *
+ * Example:
+ *
+ * $this->request->allowMethod('post');
+ * or
+ * $this->request->allowMethod(['post', 'delete']);
+ *
+ * If the request would be GET, response header "Allow: POST, DELETE" will be set
+ * and a 405 error will be returned.
+ *
+ * @param string|array $methods Allowed HTTP request methods.
+ * @return true
+ * @throws \Cake\Http\Exception\MethodNotAllowedException
+ */
+ public function allowMethod($methods): bool
+ {
+ $methods = (array)$methods;
+ foreach ($methods as $method) {
+ if ($this->is($method)) {
+ return true;
+ }
+ }
+ $allowed = strtoupper(implode(', ', $methods));
+ $e = new MethodNotAllowedException();
+ $e->setHeader('Allow', $allowed);
+ throw $e;
+ }
+
+ /**
+ * Update the request with a new request data element.
+ *
+ * Returns an updated request object. This method returns
+ * a *new* request object and does not mutate the request in-place.
+ *
+ * Use `withParsedBody()` if you need to replace the all request data.
+ *
+ * @param string $name The dot separated path to insert $value at.
+ * @param mixed $value The value to insert into the request data.
+ * @return static
+ */
+ public function withData(string $name, $value)
+ {
+ $copy = clone $this;
+
+ if (is_array($copy->data)) {
+ $copy->data = Hash::insert($copy->data, $name, $value);
+ }
+
+ return $copy;
+ }
+
+ /**
+ * Update the request removing a data element.
+ *
+ * Returns an updated request object. This method returns
+ * a *new* request object and does not mutate the request in-place.
+ *
+ * @param string $name The dot separated path to remove.
+ * @return static
+ */
+ public function withoutData(string $name)
+ {
+ $copy = clone $this;
+
+ if (is_array($copy->data)) {
+ $copy->data = Hash::remove($copy->data, $name);
+ }
+
+ return $copy;
+ }
+
+ /**
+ * Update the request with a new routing parameter
+ *
+ * Returns an updated request object. This method returns
+ * a *new* request object and does not mutate the request in-place.
+ *
+ * @param string $name The dot separated path to insert $value at.
+ * @param mixed $value The value to insert into the the request parameters.
+ * @return static
+ */
+ public function withParam(string $name, $value)
+ {
+ $copy = clone $this;
+ $copy->params = Hash::insert($copy->params, $name, $value);
+
+ return $copy;
+ }
+
+ /**
+ * Safely access the values in $this->params.
+ *
+ * @param string $name The name or dotted path to parameter.
+ * @param mixed $default The default value if `$name` is not set. Default `null`.
+ * @return mixed
+ */
+ public function getParam(string $name, $default = null)
+ {
+ return Hash::get($this->params, $name, $default);
+ }
+
+ /**
+ * Return an instance with the specified request attribute.
+ *
+ * @param string $name The attribute name.
+ * @param mixed $value The value of the attribute.
+ * @return static
+ */
+ public function withAttribute($name, $value)
+ {
+ $new = clone $this;
+ if (in_array($name, $this->emulatedAttributes, true)) {
+ $new->{$name} = $value;
+ } else {
+ $new->attributes[$name] = $value;
+ }
+
+ return $new;
+ }
+
+ /**
+ * Return an instance without the specified request attribute.
+ *
+ * @param string $name The attribute name.
+ * @return static
+ * @throws \InvalidArgumentException
+ */
+ public function withoutAttribute($name)
+ {
+ $new = clone $this;
+ if (in_array($name, $this->emulatedAttributes, true)) {
+ throw new InvalidArgumentException(
+ "You cannot unset '$name'. It is a required CakePHP attribute."
+ );
+ }
+ unset($new->attributes[$name]);
+
+ return $new;
+ }
+
+ /**
+ * Read an attribute from the request, or get the default
+ *
+ * @param string $name The attribute name.
+ * @param mixed|null $default The default value if the attribute has not been set.
+ * @return mixed
+ */
+ public function getAttribute($name, $default = null)
+ {
+ if (in_array($name, $this->emulatedAttributes, true)) {
+ if ($name === 'here') {
+ return $this->base . $this->uri->getPath();
+ }
+
+ return $this->{$name};
+ }
+ if (array_key_exists($name, $this->attributes)) {
+ return $this->attributes[$name];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Get all the attributes in the request.
+ *
+ * This will include the params, webroot, base, and here attributes that CakePHP
+ * provides.
+ *
+ * @return array
+ */
+ public function getAttributes(): array
+ {
+ $emulated = [
+ 'params' => $this->params,
+ 'webroot' => $this->webroot,
+ 'base' => $this->base,
+ 'here' => $this->base . $this->uri->getPath(),
+ ];
+
+ return $this->attributes + $emulated;
+ }
+
+ /**
+ * Get the uploaded file from a dotted path.
+ *
+ * @param string $path The dot separated path to the file you want.
+ * @return \Psr\Http\Message\UploadedFileInterface|null
+ */
+ public function getUploadedFile(string $path): ?UploadedFileInterface
+ {
+ $file = Hash::get($this->uploadedFiles, $path);
+ if (!$file instanceof UploadedFile) {
+ return null;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Get the array of uploaded files from the request.
+ *
+ * @return array
+ */
+ public function getUploadedFiles(): array
+ {
+ return $this->uploadedFiles;
+ }
+
+ /**
+ * Update the request replacing the files, and creating a new instance.
+ *
+ * @param array $uploadedFiles An array of uploaded file objects.
+ * @return static
+ * @throws \InvalidArgumentException when $files contains an invalid object.
+ */
+ public function withUploadedFiles(array $uploadedFiles)
+ {
+ $this->validateUploadedFiles($uploadedFiles, '');
+ $new = clone $this;
+ $new->uploadedFiles = $uploadedFiles;
+
+ return $new;
+ }
+
+ /**
+ * Recursively validate uploaded file data.
+ *
+ * @param array $uploadedFiles The new files array to validate.
+ * @param string $path The path thus far.
+ * @return void
+ * @throws \InvalidArgumentException If any leaf elements are not valid files.
+ */
+ protected function validateUploadedFiles(array $uploadedFiles, string $path): void
+ {
+ foreach ($uploadedFiles as $key => $file) {
+ if (is_array($file)) {
+ $this->validateUploadedFiles($file, $key . '.');
+ continue;
+ }
+
+ if (!$file instanceof UploadedFileInterface) {
+ throw new InvalidArgumentException("Invalid file at '{$path}{$key}'");
+ }
+ }
+ }
+
+ /**
+ * Gets the body of the message.
+ *
+ * @return \Psr\Http\Message\StreamInterface Returns the body as a stream.
+ */
+ public function getBody(): StreamInterface
+ {
+ return $this->stream;
+ }
+
+ /**
+ * Return an instance with the specified message body.
+ *
+ * @param \Psr\Http\Message\StreamInterface $body The new request body
+ * @return static
+ */
+ public function withBody(StreamInterface $body)
+ {
+ $new = clone $this;
+ $new->stream = $body;
+
+ return $new;
+ }
+
+ /**
+ * Retrieves the URI instance.
+ *
+ * @return \Psr\Http\Message\UriInterface Returns a UriInterface instance
+ * representing the URI of the request.
+ */
+ public function getUri(): UriInterface
+ {
+ return $this->uri;
+ }
+
+ /**
+ * Return an instance with the specified uri
+ *
+ * *Warning* Replacing the Uri will not update the `base`, `webroot`,
+ * and `url` attributes.
+ *
+ * @param \Psr\Http\Message\UriInterface $uri The new request uri
+ * @param bool $preserveHost Whether or not the host should be retained.
+ * @return static
+ */
+ public function withUri(UriInterface $uri, $preserveHost = false)
+ {
+ $new = clone $this;
+ $new->uri = $uri;
+
+ if ($preserveHost && $this->hasHeader('Host')) {
+ return $new;
+ }
+
+ $host = $uri->getHost();
+ if (!$host) {
+ return $new;
+ }
+ $port = $uri->getPort();
+ if ($port) {
+ $host .= ':' . $port;
+ }
+ $new->_environment['HTTP_HOST'] = $host;
+
+ return $new;
+ }
+
+ /**
+ * Create a new instance with a specific request-target.
+ *
+ * You can use this method to overwrite the request target that is
+ * inferred from the request's Uri. This also lets you change the request
+ * target's form to an absolute-form, authority-form or asterisk-form
+ *
+ * @link https://tools.ietf.org/html/rfc7230#section-2.7 (for the various
+ * request-target forms allowed in request messages)
+ * @param string $requestTarget The request target.
+ * @return static
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ public function withRequestTarget($requestTarget)
+ {
+ $new = clone $this;
+ $new->requestTarget = $requestTarget;
+
+ return $new;
+ }
+
+ /**
+ * Retrieves the request's target.
+ *
+ * Retrieves the message's request-target either as it was requested,
+ * or as set with `withRequestTarget()`. By default this will return the
+ * application relative path without base directory, and the query string
+ * defined in the SERVER environment.
+ *
+ * @return string
+ */
+ public function getRequestTarget(): string
+ {
+ if ($this->requestTarget !== null) {
+ return $this->requestTarget;
+ }
+
+ $target = $this->uri->getPath();
+ if ($this->uri->getQuery()) {
+ $target .= '?' . $this->uri->getQuery();
+ }
+
+ if (empty($target)) {
+ $target = '/';
+ }
+
+ return $target;
+ }
+
+ /**
+ * Get the path of current request.
+ *
+ * @return string
+ * @since 3.6.1
+ */
+ public function getPath(): string
+ {
+ if ($this->requestTarget === null) {
+ return $this->uri->getPath();
+ }
+
+ [$path] = explode('?', $this->requestTarget);
+
+ return $path;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php b/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php
new file mode 100644
index 000000000..9a39a4cf9
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/ServerRequestFactory.php
@@ -0,0 +1,354 @@
+ 'php',
+ 'cookiePath' => $uri->webroot,
+ ];
+ $session = Session::create($sessionConfig);
+
+ /** @psalm-suppress NoInterfaceProperties */
+ $request = new ServerRequest([
+ 'environment' => $server,
+ 'uri' => $uri,
+ 'cookies' => $cookies ?: $_COOKIE,
+ 'query' => $query ?: $_GET,
+ 'webroot' => $uri->webroot,
+ 'base' => $uri->base,
+ 'session' => $session,
+ 'input' => $server['CAKEPHP_INPUT'] ?? null,
+ ]);
+
+ $request = static::marshalBodyAndRequestMethod($parsedBody ?? $_POST, $request);
+ $request = static::marshalFiles($files ?? $_FILES, $request);
+
+ return $request;
+ }
+
+ /**
+ * Sets the REQUEST_METHOD environment variable based on the simulated _method
+ * HTTP override value. The 'ORIGINAL_REQUEST_METHOD' is also preserved, if you
+ * want the read the non-simulated HTTP method the client used.
+ *
+ * Request body of content type "application/x-www-form-urlencoded" is parsed
+ * into array for PUT/PATCH/DELETE requests.
+ *
+ * @param array $parsedBody Parsed body.
+ * @param \Cake\Http\ServerRequest $request Request instance.
+ * @return \Cake\Http\ServerRequest
+ */
+ protected static function marshalBodyAndRequestMethod(array $parsedBody, ServerRequest $request): ServerRequest
+ {
+ $method = $request->getMethod();
+ $override = false;
+
+ if (
+ in_array($method, ['PUT', 'DELETE', 'PATCH'], true) &&
+ strpos((string)$request->contentType(), 'application/x-www-form-urlencoded') === 0
+ ) {
+ $data = (string)$request->getBody();
+ parse_str($data, $parsedBody);
+ }
+ if ($request->hasHeader('X-Http-Method-Override')) {
+ $parsedBody['_method'] = $request->getHeaderLine('X-Http-Method-Override');
+ $override = true;
+ }
+
+ $request = $request->withEnv('ORIGINAL_REQUEST_METHOD', $method);
+ if (isset($parsedBody['_method'])) {
+ $request = $request->withEnv('REQUEST_METHOD', $parsedBody['_method']);
+ unset($parsedBody['_method']);
+ $override = true;
+ }
+
+ if (
+ $override &&
+ !in_array($request->getMethod(), ['PUT', 'POST', 'DELETE', 'PATCH'], true)
+ ) {
+ $parsedBody = [];
+ }
+
+ return $request->withParsedBody($parsedBody);
+ }
+
+ /**
+ * Process uploaded files and move things onto the parsed body.
+ *
+ * @param array $files Files array for normalization and merging in parsed body.
+ * @param \Cake\Http\ServerRequest $request Request instance.
+ * @return \Cake\Http\ServerRequest
+ */
+ protected static function marshalFiles(array $files, ServerRequest $request): ServerRequest
+ {
+ $files = normalizeUploadedFiles($files);
+ $request = $request->withUploadedFiles($files);
+
+ $parsedBody = $request->getParsedBody();
+ if (!is_array($parsedBody)) {
+ return $request;
+ }
+
+ if (Configure::read('App.uploadedFilesAsObjects', true)) {
+ $parsedBody = Hash::merge($parsedBody, $files);
+ } else {
+ // Make a flat map that can be inserted into body for BC.
+ $fileMap = Hash::flatten($files);
+ foreach ($fileMap as $key => $file) {
+ $error = $file->getError();
+ $tmpName = '';
+ if ($error === UPLOAD_ERR_OK) {
+ $tmpName = $file->getStream()->getMetadata('uri');
+ }
+ $parsedBody = Hash::insert($parsedBody, (string)$key, [
+ 'tmp_name' => $tmpName,
+ 'error' => $error,
+ 'name' => $file->getClientFilename(),
+ 'type' => $file->getClientMediaType(),
+ 'size' => $file->getSize(),
+ ]);
+ }
+ }
+
+ return $request->withParsedBody($parsedBody);
+ }
+
+ /**
+ * Create a new server request.
+ *
+ * Note that server-params are taken precisely as given - no parsing/processing
+ * of the given values is performed, and, in particular, no attempt is made to
+ * determine the HTTP method or URI, which must be provided explicitly.
+ *
+ * @param string $method The HTTP method associated with the request.
+ * @param \Psr\Http\Message\UriInterface|string $uri The URI associated with the request. If
+ * the value is a string, the factory MUST create a UriInterface
+ * instance based on it.
+ * @param array $serverParams Array of SAPI parameters with which to seed
+ * the generated request instance.
+ * @return \Psr\Http\Message\ServerRequestInterface
+ */
+ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
+ {
+ $serverParams['REQUEST_METHOD'] = $method;
+ $options = ['environment' => $serverParams];
+
+ if ($uri instanceof UriInterface) {
+ $options['uri'] = $uri;
+ } else {
+ $options['url'] = $uri;
+ }
+
+ return new ServerRequest($options);
+ }
+
+ /**
+ * Create a new Uri instance from the provided server data.
+ *
+ * @param array $server Array of server data to build the Uri from.
+ * $_SERVER will be added into the $server parameter.
+ * @return \Psr\Http\Message\UriInterface New instance.
+ */
+ public static function createUri(array $server = []): UriInterface
+ {
+ $server += $_SERVER;
+ $server = normalizeServer($server);
+ $headers = marshalHeadersFromSapi($server);
+
+ return static::marshalUriFromSapi($server, $headers);
+ }
+
+ /**
+ * Build a UriInterface object.
+ *
+ * Add in some CakePHP specific logic/properties that help
+ * preserve backwards compatibility.
+ *
+ * @param array $server The server parameters.
+ * @param array $headers The normalized headers
+ * @return \Psr\Http\Message\UriInterface a constructed Uri
+ */
+ protected static function marshalUriFromSapi(array $server, array $headers): UriInterface
+ {
+ $uri = marshalUriFromSapi($server, $headers);
+ [$base, $webroot] = static::getBase($uri, $server);
+
+ // Look in PATH_INFO first, as this is the exact value we need prepared
+ // by PHP.
+ $pathInfo = Hash::get($server, 'PATH_INFO');
+ if ($pathInfo) {
+ $uri = $uri->withPath($pathInfo);
+ } else {
+ $uri = static::updatePath($base, $uri);
+ }
+
+ if (!$uri->getHost()) {
+ $uri = $uri->withHost('localhost');
+ }
+
+ // Splat on some extra attributes to save
+ // some method calls.
+ /** @psalm-suppress NoInterfaceProperties */
+ $uri->base = $base;
+ /** @psalm-suppress NoInterfaceProperties */
+ $uri->webroot = $webroot;
+
+ return $uri;
+ }
+
+ /**
+ * Updates the request URI to remove the base directory.
+ *
+ * @param string $base The base path to remove.
+ * @param \Psr\Http\Message\UriInterface $uri The uri to update.
+ * @return \Psr\Http\Message\UriInterface The modified Uri instance.
+ */
+ protected static function updatePath(string $base, UriInterface $uri): UriInterface
+ {
+ $path = $uri->getPath();
+ if (strlen($base) > 0 && strpos($path, $base) === 0) {
+ $path = substr($path, strlen($base));
+ }
+ if ($path === '/index.php' && $uri->getQuery()) {
+ $path = $uri->getQuery();
+ }
+ if (empty($path) || $path === '/' || $path === '//' || $path === '/index.php') {
+ $path = '/';
+ }
+ $endsWithIndex = '/' . (Configure::read('App.webroot') ?: 'webroot') . '/index.php';
+ $endsWithLength = strlen($endsWithIndex);
+ if (
+ strlen($path) >= $endsWithLength &&
+ substr($path, -$endsWithLength) === $endsWithIndex
+ ) {
+ $path = '/';
+ }
+
+ return $uri->withPath($path);
+ }
+
+ /**
+ * Calculate the base directory and webroot directory.
+ *
+ * @param \Psr\Http\Message\UriInterface $uri The Uri instance.
+ * @param array $server The SERVER data to use.
+ * @return array An array containing the [baseDir, webroot]
+ */
+ protected static function getBase(UriInterface $uri, array $server): array
+ {
+ $config = (array)Configure::read('App') + [
+ 'base' => null,
+ 'webroot' => null,
+ 'baseUrl' => null,
+ ];
+ $base = $config['base'];
+ $baseUrl = $config['baseUrl'];
+ $webroot = $config['webroot'];
+
+ if ($base !== false && $base !== null) {
+ return [$base, $base . '/'];
+ }
+
+ if (!$baseUrl) {
+ $base = dirname(Hash::get($server, 'PHP_SELF'));
+ // Clean up additional / which cause following code to fail..
+ $base = preg_replace('#/+#', '/', $base);
+
+ $indexPos = strpos($base, '/' . $webroot . '/index.php');
+ if ($indexPos !== false) {
+ $base = substr($base, 0, $indexPos) . '/' . $webroot;
+ }
+ if ($webroot === basename($base)) {
+ $base = dirname($base);
+ }
+
+ if ($base === DIRECTORY_SEPARATOR || $base === '.') {
+ $base = '';
+ }
+ $base = implode('/', array_map('rawurlencode', explode('/', $base)));
+
+ return [$base, $base . '/'];
+ }
+
+ $file = '/' . basename($baseUrl);
+ $base = dirname($baseUrl);
+
+ if ($base === DIRECTORY_SEPARATOR || $base === '.') {
+ $base = '';
+ }
+ $webrootDir = $base . '/';
+
+ $docRoot = Hash::get($server, 'DOCUMENT_ROOT');
+ $docRootContainsWebroot = strpos($docRoot, $webroot);
+
+ if (!empty($base) || !$docRootContainsWebroot) {
+ if (strpos($webrootDir, '/' . $webroot . '/') === false) {
+ $webrootDir .= $webroot . '/';
+ }
+ }
+
+ return [$base . $file, $webrootDir];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Session.php b/app/vendor/cakephp/cakephp/src/Http/Session.php
new file mode 100644
index 000000000..99792b9b2
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Session.php
@@ -0,0 +1,669 @@
+ [
+ 'ini' => [
+ 'session.use_trans_sid' => 0,
+ ],
+ ],
+ 'cake' => [
+ 'ini' => [
+ 'session.use_trans_sid' => 0,
+ 'session.serialize_handler' => 'php',
+ 'session.use_cookies' => 1,
+ 'session.save_path' => $tmp . 'sessions',
+ 'session.save_handler' => 'files',
+ ],
+ ],
+ 'cache' => [
+ 'ini' => [
+ 'session.use_trans_sid' => 0,
+ 'session.use_cookies' => 1,
+ ],
+ 'handler' => [
+ 'engine' => 'CacheSession',
+ 'config' => 'default',
+ ],
+ ],
+ 'database' => [
+ 'ini' => [
+ 'session.use_trans_sid' => 0,
+ 'session.use_cookies' => 1,
+ 'session.serialize_handler' => 'php',
+ ],
+ 'handler' => [
+ 'engine' => 'DatabaseSession',
+ ],
+ ],
+ ];
+
+ if (isset($defaults[$name])) {
+ if (
+ PHP_VERSION_ID >= 70300
+ && ($name !== 'php' || empty(ini_get('session.cookie_samesite')))
+ ) {
+ $defaults['php']['ini']['session.cookie_samesite'] = 'Lax';
+ }
+
+ return $defaults[$name];
+ }
+
+ return false;
+ }
+
+ /**
+ * Constructor.
+ *
+ * ### Configuration:
+ *
+ * - timeout: The time in minutes the session should be valid for.
+ * - cookiePath: The url path for which session cookie is set. Maps to the
+ * `session.cookie_path` php.ini config. Defaults to base path of app.
+ * - ini: A list of php.ini directives to change before the session start.
+ * - handler: An array containing at least the `engine` key. To be used as the session
+ * engine for persisting data. The rest of the keys in the array will be passed as
+ * the configuration array for the engine. You can set the `engine` key to an already
+ * instantiated session handler object.
+ *
+ * @param array $config The Configuration to apply to this session object
+ */
+ public function __construct(array $config = [])
+ {
+ $config += [
+ 'timeout' => null,
+ 'cookie' => null,
+ 'ini' => [],
+ 'handler' => [],
+ ];
+
+ if ($config['timeout']) {
+ $config['ini']['session.gc_maxlifetime'] = 60 * $config['timeout'];
+ }
+
+ if ($config['cookie']) {
+ $config['ini']['session.name'] = $config['cookie'];
+ }
+
+ if (!isset($config['ini']['session.cookie_path'])) {
+ $cookiePath = empty($config['cookiePath']) ? '/' : $config['cookiePath'];
+ $config['ini']['session.cookie_path'] = $cookiePath;
+ }
+
+ $this->options($config['ini']);
+
+ if (!empty($config['handler'])) {
+ $class = $config['handler']['engine'];
+ unset($config['handler']['engine']);
+ $this->engine($class, $config['handler']);
+ }
+
+ $this->_lifetime = (int)ini_get('session.gc_maxlifetime');
+ $this->_isCLI = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
+ session_register_shutdown();
+ }
+
+ /**
+ * Sets the session handler instance to use for this session.
+ * If a string is passed for the first argument, it will be treated as the
+ * class name and the second argument will be passed as the first argument
+ * in the constructor.
+ *
+ * If an instance of a SessionHandlerInterface is provided as the first argument,
+ * the handler will be set to it.
+ *
+ * If no arguments are passed it will return the currently configured handler instance
+ * or null if none exists.
+ *
+ * @param string|\SessionHandlerInterface|null $class The session handler to use
+ * @param array $options the options to pass to the SessionHandler constructor
+ * @return \SessionHandlerInterface|null
+ * @throws \InvalidArgumentException
+ */
+ public function engine($class = null, array $options = []): ?SessionHandlerInterface
+ {
+ if ($class === null) {
+ return $this->_engine;
+ }
+ if ($class instanceof SessionHandlerInterface) {
+ return $this->setEngine($class);
+ }
+ $className = App::className($class, 'Http/Session');
+
+ if (!$className) {
+ throw new InvalidArgumentException(
+ sprintf('The class "%s" does not exist and cannot be used as a session engine', $class)
+ );
+ }
+
+ $handler = new $className($options);
+ if (!($handler instanceof SessionHandlerInterface)) {
+ throw new InvalidArgumentException(
+ 'The chosen SessionHandler does not implement SessionHandlerInterface, it cannot be used as an engine.'
+ );
+ }
+
+ return $this->setEngine($handler);
+ }
+
+ /**
+ * Set the engine property and update the session handler in PHP.
+ *
+ * @param \SessionHandlerInterface $handler The handler to set
+ * @return \SessionHandlerInterface
+ */
+ protected function setEngine(SessionHandlerInterface $handler): SessionHandlerInterface
+ {
+ if (!headers_sent() && session_status() !== \PHP_SESSION_ACTIVE) {
+ session_set_save_handler($handler, false);
+ }
+
+ return $this->_engine = $handler;
+ }
+
+ /**
+ * Calls ini_set for each of the keys in `$options` and set them
+ * to the respective value in the passed array.
+ *
+ * ### Example:
+ *
+ * ```
+ * $session->options(['session.use_cookies' => 1]);
+ * ```
+ *
+ * @param array $options Ini options to set.
+ * @return void
+ * @throws \RuntimeException if any directive could not be set
+ */
+ public function options(array $options): void
+ {
+ if (session_status() === \PHP_SESSION_ACTIVE || headers_sent()) {
+ return;
+ }
+
+ foreach ($options as $setting => $value) {
+ if (ini_set($setting, (string)$value) === false) {
+ throw new RuntimeException(
+ sprintf('Unable to configure the session, setting %s failed.', $setting)
+ );
+ }
+ }
+ }
+
+ /**
+ * Starts the Session.
+ *
+ * @return bool True if session was started
+ * @throws \RuntimeException if the session was already started
+ */
+ public function start(): bool
+ {
+ if ($this->_started) {
+ return true;
+ }
+
+ if ($this->_isCLI) {
+ $_SESSION = [];
+ $this->id('cli');
+
+ return $this->_started = true;
+ }
+
+ if (session_status() === \PHP_SESSION_ACTIVE) {
+ throw new RuntimeException('Session was already started');
+ }
+
+ if (ini_get('session.use_cookies') && headers_sent()) {
+ return false;
+ }
+
+ if (!session_start()) {
+ throw new RuntimeException('Could not start the session');
+ }
+
+ $this->_started = true;
+
+ if ($this->_timedOut()) {
+ $this->destroy();
+
+ return $this->start();
+ }
+
+ return $this->_started;
+ }
+
+ /**
+ * Write data and close the session
+ *
+ * @return true
+ */
+ public function close(): bool
+ {
+ if (!$this->_started) {
+ return true;
+ }
+
+ if ($this->_isCLI) {
+ $this->_started = false;
+
+ return true;
+ }
+
+ if (!session_write_close()) {
+ throw new RuntimeException('Could not close the session');
+ }
+
+ $this->_started = false;
+
+ return true;
+ }
+
+ /**
+ * Determine if Session has already been started.
+ *
+ * @return bool True if session has been started.
+ */
+ public function started(): bool
+ {
+ return $this->_started || session_status() === \PHP_SESSION_ACTIVE;
+ }
+
+ /**
+ * Returns true if given variable name is set in session.
+ *
+ * @param string|null $name Variable name to check for
+ * @return bool True if variable is there
+ */
+ public function check(?string $name = null): bool
+ {
+ if ($this->_hasSession() && !$this->started()) {
+ $this->start();
+ }
+
+ if (!isset($_SESSION)) {
+ return false;
+ }
+
+ if ($name === null) {
+ return (bool)$_SESSION;
+ }
+
+ return Hash::get($_SESSION, $name) !== null;
+ }
+
+ /**
+ * Returns given session variable, or all of them, if no parameters given.
+ *
+ * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
+ * @param mixed $default The return value when the path does not exist
+ * @return mixed|null The value of the session variable, or default value if a session
+ * is not available, can't be started, or provided $name is not found in the session.
+ */
+ public function read(?string $name = null, $default = null)
+ {
+ if ($this->_hasSession() && !$this->started()) {
+ $this->start();
+ }
+
+ if (!isset($_SESSION)) {
+ return $default;
+ }
+
+ if ($name === null) {
+ return $_SESSION ?: [];
+ }
+
+ return Hash::get($_SESSION, $name, $default);
+ }
+
+ /**
+ * Returns given session variable, or throws Exception if not found.
+ *
+ * @param string $name The name of the session variable (or a path as sent to Hash.extract)
+ * @throws \RuntimeException
+ * @return mixed|null
+ */
+ public function readOrFail(string $name)
+ {
+ if (!$this->check($name)) {
+ throw new RuntimeException(sprintf('Expected session key "%s" not found.', $name));
+ }
+
+ return $this->read($name);
+ }
+
+ /**
+ * Reads and deletes a variable from session.
+ *
+ * @param string $name The key to read and remove (or a path as sent to Hash.extract).
+ * @return mixed|null The value of the session variable, null if session not available,
+ * session not started, or provided name not found in the session.
+ */
+ public function consume(string $name)
+ {
+ if (empty($name)) {
+ return null;
+ }
+ $value = $this->read($name);
+ if ($value !== null) {
+ $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Writes value to given session variable name.
+ *
+ * @param string|array $name Name of variable
+ * @param mixed $value Value to write
+ * @return void
+ */
+ public function write($name, $value = null): void
+ {
+ if (!$this->started()) {
+ $this->start();
+ }
+
+ if (!is_array($name)) {
+ $name = [$name => $value];
+ }
+
+ $data = $_SESSION ?? [];
+ foreach ($name as $key => $val) {
+ $data = Hash::insert($data, $key, $val);
+ }
+
+ /** @psalm-suppress PossiblyNullArgument */
+ $this->_overwrite($_SESSION, $data);
+ }
+
+ /**
+ * Returns the session id.
+ * Calling this method will not auto start the session. You might have to manually
+ * assert a started session.
+ *
+ * Passing an id into it, you can also replace the session id if the session
+ * has not already been started.
+ * Note that depending on the session handler, not all characters are allowed
+ * within the session id. For example, the file session handler only allows
+ * characters in the range a-z A-Z 0-9 , (comma) and - (minus).
+ *
+ * @param string|null $id Id to replace the current session id
+ * @return string Session id
+ */
+ public function id(?string $id = null): string
+ {
+ if ($id !== null && !headers_sent()) {
+ session_id($id);
+ }
+
+ return session_id();
+ }
+
+ /**
+ * Removes a variable from session.
+ *
+ * @param string $name Session variable to remove
+ * @return void
+ */
+ public function delete(string $name): void
+ {
+ if ($this->check($name)) {
+ $this->_overwrite($_SESSION, Hash::remove($_SESSION, $name));
+ }
+ }
+
+ /**
+ * Used to write new data to _SESSION, since PHP doesn't like us setting the _SESSION var itself.
+ *
+ * @param array $old Set of old variables => values
+ * @param array $new New set of variable => value
+ * @return void
+ */
+ protected function _overwrite(array &$old, array $new): void
+ {
+ if (!empty($old)) {
+ foreach ($old as $key => $var) {
+ if (!isset($new[$key])) {
+ unset($old[$key]);
+ }
+ }
+ }
+ foreach ($new as $key => $var) {
+ $old[$key] = $var;
+ }
+ }
+
+ /**
+ * Helper method to destroy invalid sessions.
+ *
+ * @return void
+ */
+ public function destroy(): void
+ {
+ if ($this->_hasSession() && !$this->started()) {
+ $this->start();
+ }
+
+ if (!$this->_isCLI && session_status() === \PHP_SESSION_ACTIVE) {
+ session_destroy();
+ }
+
+ $_SESSION = [];
+ $this->_started = false;
+ }
+
+ /**
+ * Clears the session.
+ *
+ * Optionally it also clears the session id and renews the session.
+ *
+ * @param bool $renew If session should be renewed, as well. Defaults to false.
+ * @return void
+ */
+ public function clear(bool $renew = false): void
+ {
+ $_SESSION = [];
+ if ($renew) {
+ $this->renew();
+ }
+ }
+
+ /**
+ * Returns whether a session exists
+ *
+ * @return bool
+ */
+ protected function _hasSession(): bool
+ {
+ return !ini_get('session.use_cookies')
+ || isset($_COOKIE[session_name()])
+ || $this->_isCLI
+ || (ini_get('session.use_trans_sid') && isset($_GET[session_name()]));
+ }
+
+ /**
+ * Restarts this session.
+ *
+ * @return void
+ */
+ public function renew(): void
+ {
+ if (!$this->_hasSession() || $this->_isCLI) {
+ return;
+ }
+
+ $this->start();
+ $params = session_get_cookie_params();
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ $params['secure'],
+ $params['httponly']
+ );
+
+ if (session_id() !== '') {
+ session_regenerate_id(true);
+ }
+ }
+
+ /**
+ * Returns true if the session is no longer valid because the last time it was
+ * accessed was after the configured timeout.
+ *
+ * @return bool
+ */
+ protected function _timedOut(): bool
+ {
+ $time = $this->read('Config.time');
+ $result = false;
+
+ $checkTime = $time !== null && $this->_lifetime > 0;
+ if ($checkTime && (time() - (int)$time > $this->_lifetime)) {
+ $result = true;
+ }
+
+ $this->write('Config.time', time());
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Session/CacheSession.php b/app/vendor/cakephp/cakephp/src/Http/Session/CacheSession.php
new file mode 100644
index 000000000..55790ce1c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Session/CacheSession.php
@@ -0,0 +1,133 @@
+_options = $config;
+ }
+
+ /**
+ * Method called on open of a database session.
+ *
+ * @param string $savePath The path where to store/retrieve the session.
+ * @param string $name The session name.
+ * @return bool Success
+ */
+ public function open($savePath, $name): bool
+ {
+ return true;
+ }
+
+ /**
+ * Method called on close of a database session.
+ *
+ * @return bool Success
+ */
+ public function close(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Method used to read from a cache session.
+ *
+ * @param string $id ID that uniquely identifies session in cache.
+ * @return string Session data or empty string if it does not exist.
+ */
+ public function read($id): string
+ {
+ $value = Cache::read($id, $this->_options['config']);
+
+ if (empty($value)) {
+ return '';
+ }
+
+ return $value;
+ }
+
+ /**
+ * Helper function called on write for cache sessions.
+ *
+ * @param string $id ID that uniquely identifies session in cache.
+ * @param string $data The data to be saved.
+ * @return bool True for successful write, false otherwise.
+ */
+ public function write($id, $data): bool
+ {
+ if (!$id) {
+ return false;
+ }
+
+ return Cache::write($id, $data, $this->_options['config']);
+ }
+
+ /**
+ * Method called on the destruction of a cache session.
+ *
+ * @param string $id ID that uniquely identifies session in cache.
+ * @return bool Always true.
+ */
+ public function destroy($id): bool
+ {
+ Cache::delete($id, $this->_options['config']);
+
+ return true;
+ }
+
+ /**
+ * No-op method. Always returns true since cache engine don't have garbage collection.
+ *
+ * @param int $maxlifetime Sessions that have not updated for the last maxlifetime seconds will be removed.
+ * @return bool Always true.
+ */
+ public function gc($maxlifetime): bool
+ {
+ return true;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/Session/DatabaseSession.php b/app/vendor/cakephp/cakephp/src/Http/Session/DatabaseSession.php
new file mode 100644
index 000000000..5dc76644a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/Session/DatabaseSession.php
@@ -0,0 +1,191 @@
+setTableLocator($config['tableLocator']);
+ }
+ $tableLocator = $this->getTableLocator();
+
+ if (empty($config['model'])) {
+ $config = $tableLocator->exists('Sessions') ? [] : ['table' => 'sessions', 'allowFallbackClass' => true];
+ $this->_table = $tableLocator->get('Sessions', $config);
+ } else {
+ $this->_table = $tableLocator->get($config['model']);
+ }
+
+ $this->_timeout = (int)ini_get('session.gc_maxlifetime');
+ }
+
+ /**
+ * Set the timeout value for sessions.
+ *
+ * Primarily used in testing.
+ *
+ * @param int $timeout The timeout duration.
+ * @return $this
+ */
+ public function setTimeout(int $timeout)
+ {
+ $this->_timeout = $timeout;
+
+ return $this;
+ }
+
+ /**
+ * Method called on open of a database session.
+ *
+ * @param string $savePath The path where to store/retrieve the session.
+ * @param string $name The session name.
+ * @return bool Success
+ */
+ public function open($savePath, $name): bool
+ {
+ return true;
+ }
+
+ /**
+ * Method called on close of a database session.
+ *
+ * @return bool Success
+ */
+ public function close(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Method used to read from a database session.
+ *
+ * @param string $id ID that uniquely identifies session in database.
+ * @return string Session data or empty string if it does not exist.
+ */
+ public function read($id): string
+ {
+ /** @var string $pkField */
+ $pkField = $this->_table->getPrimaryKey();
+ $result = $this->_table
+ ->find('all')
+ ->select(['data'])
+ ->where([$pkField => $id])
+ ->disableHydration()
+ ->first();
+
+ if (empty($result)) {
+ return '';
+ }
+
+ if (is_string($result['data'])) {
+ return $result['data'];
+ }
+
+ $session = stream_get_contents($result['data']);
+
+ if ($session === false) {
+ return '';
+ }
+
+ return $session;
+ }
+
+ /**
+ * Helper function called on write for database sessions.
+ *
+ * @param string $id ID that uniquely identifies session in database.
+ * @param string $data The data to be saved.
+ * @return bool True for successful write, false otherwise.
+ */
+ public function write($id, $data): bool
+ {
+ if (!$id) {
+ return false;
+ }
+
+ /** @var string $pkField */
+ $pkField = $this->_table->getPrimaryKey();
+ $session = $this->_table->newEntity([
+ $pkField => $id,
+ 'data' => $data,
+ 'expires' => time() + $this->_timeout,
+ ], ['accessibleFields' => [$pkField => true]]);
+
+ return (bool)$this->_table->save($session);
+ }
+
+ /**
+ * Method called on the destruction of a database session.
+ *
+ * @param string $id ID that uniquely identifies session in database.
+ * @return bool True for successful delete, false otherwise.
+ */
+ public function destroy($id): bool
+ {
+ /** @var string $pkField */
+ $pkField = $this->_table->getPrimaryKey();
+ $this->_table->deleteAll([$pkField => $id]);
+
+ return true;
+ }
+
+ /**
+ * Helper function called on gc for database sessions.
+ *
+ * @param int $maxlifetime Sessions that have not updated for the last maxlifetime seconds will be removed.
+ * @return bool True on success, false on failure.
+ */
+ public function gc($maxlifetime): bool
+ {
+ $this->_table->deleteAll(['expires <' => time()]);
+
+ return true;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Http/composer.json b/app/vendor/cakephp/cakephp/src/Http/composer.json
new file mode 100644
index 000000000..5802f3e21
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Http/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "cakephp/http",
+ "description": "CakePHP HTTP client and PSR7/15 middleware libraries",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "http",
+ "psr7",
+ "psr15"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/http/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/http"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/core": "^4.0",
+ "cakephp/event": "^4.0",
+ "cakephp/utility": "^4.0",
+ "composer/ca-bundle": "^1.2",
+ "psr/http-client": "^1.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "laminas/laminas-diactoros": "^2.1",
+ "laminas/laminas-httphandlerrunner": "^1.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "^1.0"
+ },
+ "suggest": {
+ "cakephp/cache": "To use cache session storage",
+ "cakephp/orm": "To use database session storage"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Http\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/ChainMessagesLoader.php b/app/vendor/cakephp/cakephp/src/I18n/ChainMessagesLoader.php
new file mode 100644
index 000000000..27ccd6cf2
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/ChainMessagesLoader.php
@@ -0,0 +1,79 @@
+_loaders = $loaders;
+ }
+
+ /**
+ * Executes this object returning the translations package as configured in
+ * the chain.
+ *
+ * @return \Cake\I18n\Package
+ * @throws \RuntimeException if any of the loaders in the chain is not a valid callable
+ */
+ public function __invoke(): Package
+ {
+ foreach ($this->_loaders as $k => $loader) {
+ if (!is_callable($loader)) {
+ throw new RuntimeException(sprintf(
+ 'Loader "%s" in the chain is not a valid callable',
+ $k
+ ));
+ }
+
+ $package = $loader();
+ if (!$package) {
+ continue;
+ }
+
+ if (!($package instanceof Package)) {
+ throw new RuntimeException(sprintf(
+ 'Loader "%s" in the chain did not return a valid Package object',
+ $k
+ ));
+ }
+
+ return $package;
+ }
+
+ return new Package();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Date.php b/app/vendor/cakephp/cakephp/src/I18n/Date.php
new file mode 100644
index 000000000..450018d00
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Date.php
@@ -0,0 +1,176 @@
+ 'day',
+ 'month' => 'day',
+ 'week' => 'day',
+ 'day' => 'day',
+ 'hour' => 'day',
+ 'minute' => 'day',
+ 'second' => 'day',
+ ];
+
+ /**
+ * The end of relative time telling
+ *
+ * @var string
+ * @see \Cake\I18n\Date::timeAgoInWords()
+ */
+ public static $wordEnd = '+1 month';
+
+ /**
+ * Create a new FrozenDate instance.
+ *
+ * You can specify the timezone for the $time parameter. This timezone will
+ * not be used in any future modifications to the Date instance.
+ *
+ * The `$timezone` parameter is ignored if `$time` is a DateTimeInterface
+ * instance.
+ *
+ * Date instances lack time components, however due to limitations in PHP's
+ * internal Datetime object the time will always be set to 00:00:00, and the
+ * timezone will always be the server local time. Normalizing the timezone allows for
+ * subtraction/addition to have deterministic results.
+ *
+ * @param string|int|\DateTime|\DateTimeImmutable|null $time Fixed or relative time
+ * @param \DateTimeZone|string|null $tz The timezone in which the date is taken.
+ * Ignored if `$time` is a DateTimeInterface instance.
+ */
+ public function __construct($time = 'now', $tz = null)
+ {
+ parent::__construct($time, $tz);
+ }
+
+ /**
+ * Returns either a relative or a formatted absolute date depending
+ * on the difference between the current date and this object.
+ *
+ * ### Options:
+ *
+ * - `from` => another Date object representing the "now" date
+ * - `format` => a fall back format if the relative time is longer than the duration specified by end
+ * - `accuracy` => Specifies how accurate the date should be described (array)
+ * - year => The format if years > 0 (default "day")
+ * - month => The format if months > 0 (default "day")
+ * - week => The format if weeks > 0 (default "day")
+ * - day => The format if weeks > 0 (default "day")
+ * - `end` => The end of relative date telling
+ * - `relativeString` => The printf compatible string when outputting relative date
+ * - `absoluteString` => The printf compatible string when outputting absolute date
+ * - `timezone` => The user timezone the timestamp should be formatted in.
+ *
+ * Relative dates look something like this:
+ *
+ * - 3 weeks, 4 days ago
+ * - 1 day ago
+ *
+ * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
+ * `i18nFormat`, see the method for the valid formatting strings.
+ *
+ * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
+ * like 'Posted ' before the function output.
+ *
+ * NOTE: If the difference is one week or more, the lowest level of accuracy is day.
+ *
+ * @param array $options Array of options.
+ * @return string Relative time string.
+ */
+ public function timeAgoInWords(array $options = []): string
+ {
+ /** @psalm-suppress UndefinedInterfaceMethod */
+ return static::getDiffFormatter()->dateAgoInWords($this, $options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php b/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php
new file mode 100644
index 000000000..ac790245e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/DateFormatTrait.php
@@ -0,0 +1,495 @@
+i18nFormat(static::$niceFormat, $timezone, $locale);
+ }
+
+ /**
+ * Returns a formatted string for this time object using the preferred format and
+ * language for the specified locale.
+ *
+ * It is possible to specify the desired format for the string to be displayed.
+ * You can either pass `IntlDateFormatter` constants as the first argument of this
+ * function, or pass a full ICU date formatting string as specified in the following
+ * resource: http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details.
+ *
+ * Additional to `IntlDateFormatter` constants and date formatting string you can use
+ * Time::UNIX_TIMESTAMP_FORMAT to get a unix timestamp
+ *
+ * ### Examples
+ *
+ * ```
+ * $time = new Time('2014-04-20 22:10');
+ * $time->i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale
+ * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format
+ * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format
+ * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10'
+ * $time->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT); // outputs '1398031800'
+ * ```
+ *
+ * You can control the default format used through `Time::setToStringFormat()`.
+ *
+ * You can read about the available IntlDateFormatter constants at
+ * https://secure.php.net/manual/en/class.intldateformatter.php
+ *
+ * If you need to display the date in a different timezone than the one being used for
+ * this Time object without altering its internal state, you can pass a timezone
+ * string or object as the second parameter.
+ *
+ * Finally, should you need to use a different locale for displaying this time object,
+ * pass a locale string as the third parameter to this function.
+ *
+ * ### Examples
+ *
+ * ```
+ * $time = new Time('2014-04-20 22:10');
+ * $time->i18nFormat(null, null, 'de-DE');
+ * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE');
+ * ```
+ *
+ * You can control the default locale used through `Time::setDefaultLocale()`.
+ * If empty, the default will be taken from the `intl.default_locale` ini config.
+ *
+ * @param string|int|int[]|null $format Format string.
+ * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
+ * in which the date will be displayed. The timezone stored for this object will not
+ * be changed.
+ * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
+ * @return string|int Formatted and translated date string
+ */
+ public function i18nFormat($format = null, $timezone = null, $locale = null)
+ {
+ if ($format === Time::UNIX_TIMESTAMP_FORMAT) {
+ return $this->getTimestamp();
+ }
+
+ $time = $this;
+
+ if ($timezone) {
+ // Handle the immutable and mutable object cases.
+ $time = clone $this;
+ $time = $time->timezone($timezone);
+ }
+
+ $format = $format ?? static::$_toStringFormat;
+ $locale = $locale ?: static::$defaultLocale;
+
+ return $this->_formatObject($time, $format, $locale);
+ }
+
+ /**
+ * Returns a translated and localized date string.
+ * Implements what IntlDateFormatter::formatObject() is in PHP 5.5+
+ *
+ * @param \DateTime|\DateTimeImmutable $date Date.
+ * @param string|int|int[] $format Format.
+ * @param string|null $locale The locale name in which the date should be displayed.
+ * @return string
+ */
+ protected function _formatObject($date, $format, ?string $locale): string
+ {
+ $pattern = '';
+
+ if (is_array($format)) {
+ [$dateFormat, $timeFormat] = $format;
+ } elseif (is_int($format)) {
+ $dateFormat = $timeFormat = $format;
+ } else {
+ $dateFormat = $timeFormat = IntlDateFormatter::FULL;
+ $pattern = $format;
+ }
+
+ if ($locale === null) {
+ $locale = I18n::getLocale();
+ }
+
+ if (
+ preg_match(
+ '/@calendar=(japanese|buddhist|chinese|persian|indian|islamic|hebrew|coptic|ethiopic)/',
+ $locale
+ )
+ ) {
+ $calendar = IntlDateFormatter::TRADITIONAL;
+ } else {
+ $calendar = IntlDateFormatter::GREGORIAN;
+ }
+
+ $timezone = $date->getTimezone()->getName();
+ $key = "{$locale}.{$dateFormat}.{$timeFormat}.{$timezone}.{$calendar}.{$pattern}";
+
+ if (!isset(static::$_formatters[$key])) {
+ if ($timezone === '+00:00' || $timezone === 'Z') {
+ $timezone = 'UTC';
+ } elseif ($timezone[0] === '+' || $timezone[0] === '-') {
+ $timezone = 'GMT' . $timezone;
+ }
+ $formatter = datefmt_create(
+ $locale,
+ $dateFormat,
+ $timeFormat,
+ $timezone,
+ $calendar,
+ $pattern
+ );
+ if ($formatter === false) {
+ throw new RuntimeException(
+ 'Your version of icu does not support creating a date formatter for ' .
+ "`$key`. You should try to upgrade libicu and the intl extension."
+ );
+ }
+ static::$_formatters[$key] = $formatter;
+ }
+
+ return static::$_formatters[$key]->format($date->format('U'));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function __toString(): string
+ {
+ return (string)$this->i18nFormat();
+ }
+
+ /**
+ * Resets the format used to the default when converting an instance of this type to
+ * a string
+ *
+ * @return void
+ */
+ public static function resetToStringFormat(): void
+ {
+ static::setToStringFormat([IntlDateFormatter::SHORT, IntlDateFormatter::SHORT]);
+ }
+
+ /**
+ * Sets the default format used when type converting instances of this type to string
+ *
+ * The format should be either the formatting constants from IntlDateFormatter as
+ * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
+ * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
+ *
+ * It is possible to provide an array of 2 constants. In this case, the first position
+ * will be used for formatting the date part of the object and the second position
+ * will be used to format the time part.
+ *
+ * @param string|int|int[] $format Format.
+ * @return void
+ */
+ public static function setToStringFormat($format): void
+ {
+ static::$_toStringFormat = $format;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public static function setJsonEncodeFormat($format): void
+ {
+ static::$_jsonEncodeFormat = $format;
+ }
+
+ /**
+ * Returns a new Time object after parsing the provided time string based on
+ * the passed or configured date time format. This method is locale dependent,
+ * Any string that is passed to this function will be interpreted as a locale
+ * dependent string.
+ *
+ * When no $format is provided, the `toString` format will be used.
+ *
+ * Unlike DateTime, the time zone of the returned instance is always converted
+ * to `$tz` (default time zone if null) even if the `$time` string specified a
+ * time zone. This is a limitation of IntlDateFormatter.
+ *
+ * If it was impossible to parse the provided time, null will be returned.
+ *
+ * Example:
+ *
+ * ```
+ * $time = Time::parseDateTime('10/13/2013 12:54am');
+ * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm');
+ * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, IntlDateFormatter::NONE]);
+ * ```
+ *
+ * @param string $time The time string to parse.
+ * @param string|int|int[]|null $format Any format accepted by IntlDateFormatter.
+ * @param \DateTimeZone|string|null $tz The timezone for the instance
+ * @return static|null
+ */
+ public static function parseDateTime(string $time, $format = null, $tz = null)
+ {
+ $format = $format ?? static::$_toStringFormat;
+ $pattern = '';
+
+ if (is_array($format)) {
+ [$dateFormat, $timeFormat] = $format;
+ } elseif (is_int($format)) {
+ $dateFormat = $timeFormat = $format;
+ } else {
+ $dateFormat = $timeFormat = IntlDateFormatter::FULL;
+ $pattern = $format;
+ }
+
+ $locale = static::$defaultLocale ?? I18n::getLocale();
+ $formatter = datefmt_create(
+ $locale,
+ $dateFormat,
+ $timeFormat,
+ $tz,
+ null,
+ $pattern
+ );
+ $formatter->setLenient(static::$lenientParsing);
+
+ $time = $formatter->parse($time);
+ if ($time !== false) {
+ $dateTime = new DateTime('@' . $time);
+
+ if (!($tz instanceof DateTimeZone)) {
+ $tz = new DateTimeZone($tz ?? date_default_timezone_get());
+ }
+ $dateTime->setTimezone($tz);
+
+ return new static($dateTime);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a new Time object after parsing the provided $date string based on
+ * the passed or configured date time format. This method is locale dependent,
+ * Any string that is passed to this function will be interpreted as a locale
+ * dependent string.
+ *
+ * When no $format is provided, the `wordFormat` format will be used.
+ *
+ * If it was impossible to parse the provided time, null will be returned.
+ *
+ * Example:
+ *
+ * ```
+ * $time = Time::parseDate('10/13/2013');
+ * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y');
+ * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT);
+ * ```
+ *
+ * @param string $date The date string to parse.
+ * @param string|int|array|null $format Any format accepted by IntlDateFormatter.
+ * @return static|null
+ */
+ public static function parseDate(string $date, $format = null)
+ {
+ if (is_int($format)) {
+ $format = [$format, IntlDateFormatter::NONE];
+ }
+ $format = $format ?: static::$wordFormat;
+
+ return static::parseDateTime($date, $format);
+ }
+
+ /**
+ * Returns a new Time object after parsing the provided $time string based on
+ * the passed or configured date time format. This method is locale dependent,
+ * Any string that is passed to this function will be interpreted as a locale
+ * dependent string.
+ *
+ * When no $format is provided, the IntlDateFormatter::SHORT format will be used.
+ *
+ * If it was impossible to parse the provided time, null will be returned.
+ *
+ * Example:
+ *
+ * ```
+ * $time = Time::parseTime('11:23pm');
+ * ```
+ *
+ * @param string $time The time string to parse.
+ * @param string|int|null $format Any format accepted by IntlDateFormatter.
+ * @return static|null
+ */
+ public static function parseTime(string $time, $format = null)
+ {
+ if (is_int($format)) {
+ $format = [IntlDateFormatter::NONE, $format];
+ }
+ $format = $format ?: [IntlDateFormatter::NONE, IntlDateFormatter::SHORT];
+
+ return static::parseDateTime($time, $format);
+ }
+
+ /**
+ * Returns a string that should be serialized when converting this object to JSON
+ *
+ * @return string|int
+ */
+ public function jsonSerialize()
+ {
+ if (static::$_jsonEncodeFormat instanceof Closure) {
+ return call_user_func(static::$_jsonEncodeFormat, $this);
+ }
+
+ return $this->i18nFormat(static::$_jsonEncodeFormat);
+ }
+
+ /**
+ * Get the difference formatter instance.
+ *
+ * @return \Cake\Chronos\DifferenceFormatterInterface
+ */
+ public static function getDiffFormatter(): DifferenceFormatterInterface
+ {
+ // Use the static property defined in chronos.
+ if (static::$diffFormatter === null) {
+ static::$diffFormatter = new RelativeTimeFormatter();
+ }
+
+ return static::$diffFormatter;
+ }
+
+ /**
+ * Set the difference formatter instance.
+ *
+ * @param \Cake\Chronos\DifferenceFormatterInterface $formatter The formatter instance when setting.
+ * @return void
+ */
+ public static function setDiffFormatter(DifferenceFormatterInterface $formatter): void
+ {
+ static::$diffFormatter = $formatter;
+ }
+
+ /**
+ * Returns the data that should be displayed when debugging this object
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ /** @psalm-suppress PossiblyNullReference */
+ return [
+ 'time' => $this->format('Y-m-d H:i:s.uP'),
+ 'timezone' => $this->getTimezone()->getName(),
+ 'fixedNowTime' => static::hasTestNow() ? static::getTestNow()->format('Y-m-d\TH:i:s.uP') : false,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Exception/I18nException.php b/app/vendor/cakephp/cakephp/src/I18n/Exception/I18nException.php
new file mode 100644
index 000000000..3b3819e3e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Exception/I18nException.php
@@ -0,0 +1,27 @@
+format($tokenValues);
+ if ($result === false) {
+ throw new I18nException($formatter->getErrorMessage(), $formatter->getErrorCode());
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Formatter/SprintfFormatter.php b/app/vendor/cakephp/cakephp/src/I18n/Formatter/SprintfFormatter.php
new file mode 100644
index 000000000..84c5d7bdd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Formatter/SprintfFormatter.php
@@ -0,0 +1,40 @@
+ $spec) {
+ $this->set($name, $spec);
+ }
+ }
+
+ /**
+ * Sets a formatter into the registry by name.
+ *
+ * @param string $name The formatter name.
+ * @param string $className A FQCN for a formatter.
+ * @return void
+ */
+ public function set(string $name, string $className): void
+ {
+ $this->registry[$name] = $className;
+ $this->converted[$name] = false;
+ }
+
+ /**
+ * Gets a formatter from the registry by name.
+ *
+ * @param string $name The formatter to retrieve.
+ * @return \Cake\I18n\FormatterInterface A formatter object.
+ * @throws \Cake\I18n\Exception\I18nException
+ */
+ public function get(string $name): FormatterInterface
+ {
+ if (!isset($this->registry[$name])) {
+ throw new I18nException("Formatter named `{$name}` has not been registered");
+ }
+
+ if (!$this->converted[$name]) {
+ $this->registry[$name] = new $this->registry[$name]();
+ $this->converted[$name] = true;
+ }
+
+ return $this->registry[$name];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/FrozenDate.php b/app/vendor/cakephp/cakephp/src/I18n/FrozenDate.php
new file mode 100644
index 000000000..a4602bfdd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/FrozenDate.php
@@ -0,0 +1,178 @@
+ 'day',
+ 'month' => 'day',
+ 'week' => 'day',
+ 'day' => 'day',
+ 'hour' => 'day',
+ 'minute' => 'day',
+ 'second' => 'day',
+ ];
+
+ /**
+ * The end of relative time telling
+ *
+ * @var string
+ * @see \Cake\I18n\Date::timeAgoInWords()
+ */
+ public static $wordEnd = '+1 month';
+
+ /**
+ * Create a new Date instance.
+ *
+ * You can specify the timezone for the $time parameter. This timezone will
+ * not be used in any future modifications to the Date instance.
+ *
+ * The `$timezone` parameter is ignored if `$time` is a DateTimeInterface
+ * instance.
+ *
+ * Date instances lack time components, however due to limitations in PHP's
+ * internal Datetime object the time will always be set to 00:00:00, and the
+ * timezone will always be the server local time. Normalizing the timezone allows for
+ * subtraction/addition to have deterministic results.
+ *
+ * @param string|int|\DateTime|\DateTimeImmutable|null $time Fixed or relative time
+ * @param \DateTimeZone|string|null $tz The timezone in which the date is taken.
+ * Ignored if `$time` is a DateTimeInterface instance.
+ */
+ public function __construct($time = 'now', $tz = null)
+ {
+ parent::__construct($time, $tz);
+ }
+
+ /**
+ * Returns either a relative or a formatted absolute date depending
+ * on the difference between the current date and this object.
+ *
+ * ### Options:
+ *
+ * - `from` => another Date object representing the "now" date
+ * - `format` => a fall back format if the relative time is longer than the duration specified by end
+ * - `accuracy` => Specifies how accurate the date should be described (array)
+ * - year => The format if years > 0 (default "day")
+ * - month => The format if months > 0 (default "day")
+ * - week => The format if weeks > 0 (default "day")
+ * - day => The format if weeks > 0 (default "day")
+ * - `end` => The end of relative date telling
+ * - `relativeString` => The printf compatible string when outputting relative date
+ * - `absoluteString` => The printf compatible string when outputting absolute date
+ * - `timezone` => The user timezone the timestamp should be formatted in.
+ *
+ * Relative dates look something like this:
+ *
+ * - 3 weeks, 4 days ago
+ * - 1 day ago
+ *
+ * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
+ * `i18nFormat`, see the method for the valid formatting strings.
+ *
+ * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
+ * like 'Posted ' before the function output.
+ *
+ * NOTE: If the difference is one week or more, the lowest level of accuracy is day.
+ *
+ * @param array $options Array of options.
+ * @return string Relative time string.
+ */
+ public function timeAgoInWords(array $options = []): string
+ {
+ /** @psalm-suppress UndefinedInterfaceMethod */
+ return static::getDiffFormatter()->dateAgoInWords($this, $options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/FrozenTime.php b/app/vendor/cakephp/cakephp/src/I18n/FrozenTime.php
new file mode 100644
index 000000000..2bf669c0e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/FrozenTime.php
@@ -0,0 +1,263 @@
+ 'day',
+ 'month' => 'day',
+ 'week' => 'day',
+ 'day' => 'hour',
+ 'hour' => 'minute',
+ 'minute' => 'minute',
+ 'second' => 'second',
+ ];
+
+ /**
+ * The end of relative time telling
+ *
+ * @var string
+ * @see \Cake\I18n\FrozenTime::timeAgoInWords()
+ */
+ public static $wordEnd = '+1 month';
+
+ /**
+ * serialise the value as a Unix Timestamp
+ *
+ * @var string
+ */
+ public const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat';
+
+ /**
+ * Create a new immutable time instance.
+ *
+ * @param string|int|\DateTimeInterface|null $time Fixed or relative time
+ * @param \DateTimeZone|string|null $tz The timezone for the instance
+ */
+ public function __construct($time = null, $tz = null)
+ {
+ if ($time instanceof DateTimeInterface) {
+ $tz = $time->getTimezone();
+ $time = $time->format('Y-m-d H:i:s.u');
+ }
+
+ if (is_numeric($time)) {
+ $time = '@' . $time;
+ }
+
+ parent::__construct($time, $tz);
+ }
+
+ /**
+ * Returns either a relative or a formatted absolute date depending
+ * on the difference between the current time and this object.
+ *
+ * ### Options:
+ *
+ * - `from` => another Time object representing the "now" time
+ * - `format` => a fall back format if the relative time is longer than the duration specified by end
+ * - `accuracy` => Specifies how accurate the date should be described (array)
+ * - year => The format if years > 0 (default "day")
+ * - month => The format if months > 0 (default "day")
+ * - week => The format if weeks > 0 (default "day")
+ * - day => The format if weeks > 0 (default "hour")
+ * - hour => The format if hours > 0 (default "minute")
+ * - minute => The format if minutes > 0 (default "minute")
+ * - second => The format if seconds > 0 (default "second")
+ * - `end` => The end of relative time telling
+ * - `relativeString` => The printf compatible string when outputting relative time
+ * - `absoluteString` => The printf compatible string when outputting absolute time
+ * - `timezone` => The user timezone the timestamp should be formatted in.
+ *
+ * Relative dates look something like this:
+ *
+ * - 3 weeks, 4 days ago
+ * - 15 seconds ago
+ *
+ * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
+ * `i18nFormat`, see the method for the valid formatting strings
+ *
+ * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
+ * like 'Posted ' before the function output.
+ *
+ * NOTE: If the difference is one week or more, the lowest level of accuracy is day
+ *
+ * @param array $options Array of options.
+ * @return string Relative time string.
+ */
+ public function timeAgoInWords(array $options = []): string
+ {
+ /** @psalm-suppress UndefinedInterfaceMethod */
+ return static::getDiffFormatter()->timeAgoInWords($this, $options);
+ }
+
+ /**
+ * Get list of timezone identifiers
+ *
+ * @param int|string|null $filter A regex to filter identifier
+ * Or one of DateTimeZone class constants
+ * @param string|null $country A two-letter ISO 3166-1 compatible country code.
+ * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY
+ * @param bool|array $options If true (default value) groups the identifiers list by primary region.
+ * Otherwise, an array containing `group`, `abbr`, `before`, and `after`
+ * keys. Setting `group` and `abbr` to true will group results and append
+ * timezone abbreviation in the display value. Set `before` and `after`
+ * to customize the abbreviation wrapper.
+ * @return array List of timezone identifiers
+ * @since 2.2
+ */
+ public static function listTimezones($filter = null, ?string $country = null, $options = []): array
+ {
+ if (is_bool($options)) {
+ $options = [
+ 'group' => $options,
+ ];
+ }
+ $defaults = [
+ 'group' => true,
+ 'abbr' => false,
+ 'before' => ' - ',
+ 'after' => null,
+ ];
+ $options += $defaults;
+ $group = $options['group'];
+
+ $regex = null;
+ if (is_string($filter)) {
+ $regex = $filter;
+ $filter = null;
+ }
+ if ($filter === null) {
+ $filter = DateTimeZone::ALL;
+ }
+ $identifiers = DateTimeZone::listIdentifiers($filter, (string)$country) ?: [];
+
+ if ($regex) {
+ foreach ($identifiers as $key => $tz) {
+ if (!preg_match($regex, $tz)) {
+ unset($identifiers[$key]);
+ }
+ }
+ }
+
+ if ($group) {
+ $groupedIdentifiers = [];
+ $now = time();
+ $before = $options['before'];
+ $after = $options['after'];
+ foreach ($identifiers as $tz) {
+ $abbr = '';
+ if ($options['abbr']) {
+ $dateTimeZone = new DateTimeZone($tz);
+ $trans = $dateTimeZone->getTransitions($now, $now);
+ $abbr = isset($trans[0]['abbr']) ?
+ $before . $trans[0]['abbr'] . $after :
+ '';
+ }
+ $item = explode('/', $tz, 2);
+ if (isset($item[1])) {
+ $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr;
+ } else {
+ $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr];
+ }
+ }
+
+ return $groupedIdentifiers;
+ }
+
+ return array_combine($identifiers, $identifiers);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/I18n.php b/app/vendor/cakephp/cakephp/src/I18n/I18n.php
new file mode 100644
index 000000000..2e5c539d7
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/I18n.php
@@ -0,0 +1,307 @@
+ IcuFormatter::class,
+ 'sprintf' => SprintfFormatter::class,
+ ]),
+ static::getLocale()
+ );
+
+ if (class_exists(Cache::class)) {
+ static::$_collection->setCacher(Cache::pool('_cake_core_'));
+ }
+
+ return static::$_collection;
+ }
+
+ /**
+ * Sets a translator.
+ *
+ * Configures future translators, this is achieved by passing a callable
+ * as the last argument of this function.
+ *
+ * ### Example:
+ *
+ * ```
+ * I18n::setTranslator('default', function () {
+ * $package = new \Cake\I18n\Package();
+ * $package->setMessages([
+ * 'Cake' => 'Gâteau'
+ * ]);
+ * return $package;
+ * }, 'fr_FR');
+ *
+ * $translator = I18n::getTranslator('default', 'fr_FR');
+ * echo $translator->translate('Cake');
+ * ```
+ *
+ * You can also use the `Cake\I18n\MessagesFileLoader` class to load a specific
+ * file from a folder. For example for loading a `my_translations.po` file from
+ * the `resources/locales/custom` folder, you would do:
+ *
+ * ```
+ * I18n::setTranslator(
+ * 'default',
+ * new MessagesFileLoader('my_translations', 'custom', 'po'),
+ * 'fr_FR'
+ * );
+ * ```
+ *
+ * @param string $name The domain of the translation messages.
+ * @param callable $loader A callback function or callable class responsible for
+ * constructing a translations package instance.
+ * @param string|null $locale The locale for the translator.
+ * @return void
+ */
+ public static function setTranslator(string $name, callable $loader, ?string $locale = null): void
+ {
+ $locale = $locale ?: static::getLocale();
+
+ $translators = static::translators();
+ $loader = $translators->setLoaderFallback($name, $loader);
+ $packages = $translators->getPackages();
+ $packages->set($name, $locale, $loader);
+ }
+
+ /**
+ * Returns an instance of a translator that was configured for the name and locale.
+ *
+ * If no locale is passed then it takes the value returned by the `getLocale()` method.
+ *
+ * @param string $name The domain of the translation messages.
+ * @param string|null $locale The locale for the translator.
+ * @return \Cake\I18n\Translator The configured translator.
+ * @throws \Cake\I18n\Exception\I18nException
+ */
+ public static function getTranslator(string $name = 'default', ?string $locale = null): Translator
+ {
+ $translators = static::translators();
+
+ if ($locale) {
+ $currentLocale = $translators->getLocale();
+ $translators->setLocale($locale);
+ }
+
+ $translator = $translators->get($name);
+ if ($translator === null) {
+ throw new I18nException(sprintf(
+ 'Translator for domain "%s" could not be found.',
+ $name
+ ));
+ }
+
+ if (isset($currentLocale)) {
+ $translators->setLocale($currentLocale);
+ }
+
+ return $translator;
+ }
+
+ /**
+ * Registers a callable object that can be used for creating new translator
+ * instances for the same translations domain. Loaders will be invoked whenever
+ * a translator object is requested for a domain that has not been configured or
+ * loaded already.
+ *
+ * Registering loaders is useful when you need to lazily use translations in multiple
+ * different locales for the same domain, and don't want to use the built-in
+ * translation service based of `gettext` files.
+ *
+ * Loader objects will receive two arguments: The domain name that needs to be
+ * built, and the locale that is requested. These objects can assemble the messages
+ * from any source, but must return an `Cake\I18n\Package` object.
+ *
+ * ### Example:
+ *
+ * ```
+ * use Cake\I18n\MessagesFileLoader;
+ * I18n::config('my_domain', function ($name, $locale) {
+ * // Load resources/locales/$locale/filename.po
+ * $fileLoader = new MessagesFileLoader('filename', $locale, 'po');
+ * return $fileLoader();
+ * });
+ * ```
+ *
+ * You can also assemble the package object yourself:
+ *
+ * ```
+ * use Cake\I18n\Package;
+ * I18n::config('my_domain', function ($name, $locale) {
+ * $package = new Package('default');
+ * $messages = (...); // Fetch messages for locale from external service.
+ * $package->setMessages($message);
+ * $package->setFallback('default');
+ * return $package;
+ * });
+ * ```
+ *
+ * @param string $name The name of the translator to create a loader for
+ * @param callable $loader A callable object that should return a Package
+ * instance to be used for assembling a new translator.
+ * @return void
+ */
+ public static function config(string $name, callable $loader): void
+ {
+ static::translators()->registerLoader($name, $loader);
+ }
+
+ /**
+ * Sets the default locale to use for future translator instances.
+ * This also affects the `intl.default_locale` PHP setting.
+ *
+ * @param string $locale The name of the locale to set as default.
+ * @return void
+ */
+ public static function setLocale(string $locale): void
+ {
+ static::getDefaultLocale();
+ Locale::setDefault($locale);
+ if (isset(static::$_collection)) {
+ static::translators()->setLocale($locale);
+ }
+ }
+
+ /**
+ * Will return the currently configure locale as stored in the
+ * `intl.default_locale` PHP setting.
+ *
+ * @return string The name of the default locale.
+ */
+ public static function getLocale(): string
+ {
+ static::getDefaultLocale();
+ $current = Locale::getDefault();
+ if ($current === '') {
+ $current = static::DEFAULT_LOCALE;
+ Locale::setDefault($current);
+ }
+
+ return $current;
+ }
+
+ /**
+ * Returns the default locale.
+ *
+ * This returns the default locale before any modifications, i.e.
+ * the value as stored in the `intl.default_locale` PHP setting before
+ * any manipulation by this class.
+ *
+ * @return string
+ */
+ public static function getDefaultLocale(): string
+ {
+ if (static::$_defaultLocale === null) {
+ static::$_defaultLocale = Locale::getDefault() ?: static::DEFAULT_LOCALE;
+ }
+
+ return static::$_defaultLocale;
+ }
+
+ /**
+ * Returns the currently configured default formatter.
+ *
+ * @return string The name of the formatter.
+ */
+ public static function getDefaultFormatter(): string
+ {
+ return static::translators()->defaultFormatter();
+ }
+
+ /**
+ * Sets the name of the default messages formatter to use for future
+ * translator instances. By default the `default` and `sprintf` formatters
+ * are available.
+ *
+ * @param string $name The name of the formatter to use.
+ * @return void
+ */
+ public static function setDefaultFormatter(string $name): void
+ {
+ static::translators()->defaultFormatter($name);
+ }
+
+ /**
+ * Set if the domain fallback is used.
+ *
+ * @param bool $enable flag to enable or disable fallback
+ * @return void
+ */
+ public static function useFallback(bool $enable = true): void
+ {
+ static::translators()->useFallback($enable);
+ }
+
+ /**
+ * Destroys all translator instances and creates a new empty translations
+ * collection.
+ *
+ * @return void
+ */
+ public static function clear(): void
+ {
+ static::$_collection = null;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/I18nDateTimeInterface.php b/app/vendor/cakephp/cakephp/src/I18n/I18nDateTimeInterface.php
new file mode 100644
index 000000000..88b5060ce
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/I18nDateTimeInterface.php
@@ -0,0 +1,236 @@
+i18nFormat(); // outputs '4/20/14, 10:10 PM' for the en-US locale
+ * $time->i18nFormat(\IntlDateFormatter::FULL); // Use the full date and time format
+ * $time->i18nFormat([\IntlDateFormatter::FULL, \IntlDateFormatter::SHORT]); // Use full date but short time format
+ * $time->i18nFormat('yyyy-MM-dd HH:mm:ss'); // outputs '2014-04-20 22:10'
+ * $time->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT); // outputs '1398031800'
+ * ```
+ *
+ * If you wish to control the default format to be used for this method, you can alter
+ * the value of the static `Time::$defaultLocale` variable and set it to one of the
+ * possible formats accepted by this function.
+ *
+ * You can read about the available IntlDateFormatter constants at
+ * https://secure.php.net/manual/en/class.intldateformatter.php
+ *
+ * If you need to display the date in a different timezone than the one being used for
+ * this Time object without altering its internal state, you can pass a timezone
+ * string or object as the second parameter.
+ *
+ * Finally, should you need to use a different locale for displaying this time object,
+ * pass a locale string as the third parameter to this function.
+ *
+ * ### Examples
+ *
+ * ```
+ * $time = new Time('2014-04-20 22:10');
+ * $time->i18nFormat(null, null, 'de-DE');
+ * $time->i18nFormat(\IntlDateFormatter::FULL, 'Europe/Berlin', 'de-DE');
+ * ```
+ *
+ * You can control the default locale to be used by setting the static variable
+ * `Time::$defaultLocale` to a valid locale string. If empty, the default will be
+ * taken from the `intl.default_locale` ini config.
+ *
+ * @param string|int|null $format Format string.
+ * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
+ * in which the date will be displayed. The timezone stored for this object will not
+ * be changed.
+ * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
+ * @return string|int Formatted and translated date string
+ */
+ public function i18nFormat($format = null, $timezone = null, $locale = null);
+
+ /**
+ * Resets the format used to the default when converting an instance of this type to
+ * a string
+ *
+ * @return void
+ */
+ public static function resetToStringFormat(): void;
+
+ /**
+ * Sets the default format used when type converting instances of this type to string
+ *
+ * @param string|int|int[] $format Format.
+ * @return void
+ */
+ public static function setToStringFormat($format): void;
+
+ /**
+ * Sets the default format used when converting this object to JSON
+ *
+ * The format should be either the formatting constants from IntlDateFormatter as
+ * described in (https://secure.php.net/manual/en/class.intldateformatter.php) or a pattern
+ * as specified in (http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details)
+ *
+ * It is possible to provide an array of 2 constants. In this case, the first position
+ * will be used for formatting the date part of the object and the second position
+ * will be used to format the time part.
+ *
+ * Alternatively, the format can provide a callback. In this case, the callback
+ * can receive this datetime object and return a formatted string.
+ *
+ * @see \Cake\I18n\Time::i18nFormat()
+ * @param string|array|int|\Closure $format Format.
+ * @return void
+ */
+ public static function setJsonEncodeFormat($format): void;
+
+ /**
+ * Returns a new Time object after parsing the provided time string based on
+ * the passed or configured date time format. This method is locale dependent,
+ * Any string that is passed to this function will be interpreted as a locale
+ * dependent string.
+ *
+ * When no $format is provided, the `toString` format will be used.
+ *
+ * If it was impossible to parse the provided time, null will be returned.
+ *
+ * Example:
+ *
+ * ```
+ * $time = Time::parseDateTime('10/13/2013 12:54am');
+ * $time = Time::parseDateTime('13 Oct, 2013 13:54', 'dd MMM, y H:mm');
+ * $time = Time::parseDateTime('10/10/2015', [IntlDateFormatter::SHORT, -1]);
+ * ```
+ *
+ * @param string $time The time string to parse.
+ * @param string|int[]|null $format Any format accepted by IntlDateFormatter.
+ * @param \DateTimeZone|string|null $tz The timezone for the instance
+ * @return static|null
+ * @throws \InvalidArgumentException If $format is a single int instead of array of constants
+ */
+ public static function parseDateTime(string $time, $format = null, $tz = null);
+
+ /**
+ * Returns a new Time object after parsing the provided $date string based on
+ * the passed or configured date time format. This method is locale dependent,
+ * Any string that is passed to this function will be interpreted as a locale
+ * dependent string.
+ *
+ * When no $format is provided, the `wordFormat` format will be used.
+ *
+ * If it was impossible to parse the provided time, null will be returned.
+ *
+ * Example:
+ *
+ * ```
+ * $time = Time::parseDate('10/13/2013');
+ * $time = Time::parseDate('13 Oct, 2013', 'dd MMM, y');
+ * $time = Time::parseDate('13 Oct, 2013', IntlDateFormatter::SHORT);
+ * ```
+ *
+ * @param string $date The date string to parse.
+ * @param string|int|array|null $format Any format accepted by IntlDateFormatter.
+ * @return static|null
+ */
+ public static function parseDate(string $date, $format = null);
+
+ /**
+ * Returns a new Time object after parsing the provided $time string based on
+ * the passed or configured date time format. This method is locale dependent,
+ * Any string that is passed to this function will be interpreted as a locale
+ * dependent string.
+ *
+ * When no $format is provided, the IntlDateFormatter::SHORT format will be used.
+ *
+ * If it was impossible to parse the provided time, null will be returned.
+ *
+ * Example:
+ *
+ * ```
+ * $time = Time::parseTime('11:23pm');
+ * ```
+ *
+ * @param string $time The time string to parse.
+ * @param string|int|null $format Any format accepted by IntlDateFormatter.
+ * @return static|null
+ */
+ public static function parseTime(string $time, $format = null);
+
+ /**
+ * Get the difference formatter instance.
+ *
+ * @return \Cake\Chronos\DifferenceFormatterInterface The formatter instance.
+ */
+ public static function getDiffFormatter(): DifferenceFormatterInterface;
+
+ /**
+ * Set the difference formatter instance.
+ *
+ * @param \Cake\Chronos\DifferenceFormatterInterface $formatter The formatter instance when setting.
+ * @return void
+ */
+ public static function setDiffFormatter(DifferenceFormatterInterface $formatter): void;
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/LICENSE.txt b/app/vendor/cakephp/cakephp/src/I18n/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php b/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php
new file mode 100644
index 000000000..564077d6c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/MessagesFileLoader.php
@@ -0,0 +1,183 @@
+_name = $name;
+ $this->_locale = $locale;
+ $this->_extension = $extension;
+ }
+
+ /**
+ * Loads the translation file and parses it. Returns an instance of a translations
+ * package containing the messages loaded from the file.
+ *
+ * @return \Cake\I18n\Package|false
+ * @throws \RuntimeException if no file parser class could be found for the specified
+ * file extension.
+ */
+ public function __invoke()
+ {
+ $folders = $this->translationsFolders();
+ $ext = $this->_extension;
+ $file = false;
+
+ $fileName = $this->_name;
+ $pos = strpos($fileName, '/');
+ if ($pos !== false) {
+ $fileName = substr($fileName, $pos + 1);
+ }
+ foreach ($folders as $folder) {
+ $path = $folder . $fileName . ".$ext";
+ if (is_file($path)) {
+ $file = $path;
+ break;
+ }
+ }
+
+ if (!$file) {
+ return false;
+ }
+
+ $name = ucfirst($ext);
+ $class = App::className($name, 'I18n\Parser', 'FileParser');
+
+ if (!$class) {
+ throw new RuntimeException(sprintf('Could not find class %s', "{$name}FileParser"));
+ }
+
+ $messages = (new $class())->parse($file);
+ $package = new Package('default');
+ $package->setMessages($messages);
+
+ return $package;
+ }
+
+ /**
+ * Returns the folders where the file should be looked for according to the locale
+ * and package name.
+ *
+ * @return string[] The list of folders where the translation file should be looked for
+ */
+ public function translationsFolders(): array
+ {
+ $locale = Locale::parseLocale($this->_locale) + ['region' => null];
+
+ $folders = [
+ implode('_', [$locale['language'], $locale['region']]),
+ $locale['language'],
+ ];
+
+ $searchPaths = [];
+
+ $localePaths = App::path('locales');
+ if (empty($localePaths) && defined('APP')) {
+ $localePaths[] = ROOT . 'resources' . DIRECTORY_SEPARATOR . 'locales' . DIRECTORY_SEPARATOR;
+ }
+ foreach ($localePaths as $path) {
+ foreach ($folders as $folder) {
+ $searchPaths[] = $path . $folder . DIRECTORY_SEPARATOR;
+ }
+ }
+
+ // If space is not added after slash, the character after it remains lowercased
+ $pluginName = Inflector::camelize(str_replace('/', '/ ', $this->_name));
+ if (Plugin::isLoaded($pluginName)) {
+ $basePath = App::path('locales', $pluginName)[0];
+ foreach ($folders as $folder) {
+ $searchPaths[] = $basePath . $folder . DIRECTORY_SEPARATOR;
+ }
+ }
+
+ return $searchPaths;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Middleware/LocaleSelectorMiddleware.php b/app/vendor/cakephp/cakephp/src/I18n/Middleware/LocaleSelectorMiddleware.php
new file mode 100644
index 000000000..6d32035c2
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Middleware/LocaleSelectorMiddleware.php
@@ -0,0 +1,72 @@
+locales = $locales;
+ }
+
+ /**
+ * Set locale based on request headers.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $locale = Locale::acceptFromHttp($request->getHeaderLine('Accept-Language'));
+ if (!$locale) {
+ return $handler->handle($request);
+ }
+ if ($this->locales !== ['*']) {
+ $locale = Locale::lookup($this->locales, $locale, true);
+ }
+ if ($locale || $this->locales === ['*']) {
+ I18n::setLocale($locale);
+ }
+
+ return $handler->handle($request);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Number.php b/app/vendor/cakephp/cakephp/src/I18n/Number.php
new file mode 100644
index 000000000..5312aac4f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Number.php
@@ -0,0 +1,488 @@
+ $precision, 'places' => $precision] + $options);
+
+ return $formatter->format($value);
+ }
+
+ /**
+ * Returns a formatted-for-humans file size.
+ *
+ * @param int|string $size Size in bytes
+ * @return string Human readable size
+ * @link https://book.cakephp.org/4/en/core-libraries/number.html#interacting-with-human-readable-values
+ */
+ public static function toReadableSize($size): string
+ {
+ $size = (int)$size;
+
+ switch (true) {
+ case $size < 1024:
+ return __dn('cake', '{0,number,integer} Byte', '{0,number,integer} Bytes', $size, $size);
+ case round($size / 1024) < 1024:
+ return __d('cake', '{0,number,#,###.##} KB', $size / 1024);
+ case round($size / 1024 / 1024, 2) < 1024:
+ return __d('cake', '{0,number,#,###.##} MB', $size / 1024 / 1024);
+ case round($size / 1024 / 1024 / 1024, 2) < 1024:
+ return __d('cake', '{0,number,#,###.##} GB', $size / 1024 / 1024 / 1024);
+ default:
+ return __d('cake', '{0,number,#,###.##} TB', $size / 1024 / 1024 / 1024 / 1024);
+ }
+ }
+
+ /**
+ * Formats a number into a percentage string.
+ *
+ * Options:
+ *
+ * - `multiply`: Multiply the input value by 100 for decimal percentages.
+ * - `locale`: The locale name to use for formatting the number, e.g. fr_FR
+ *
+ * @param float|string $value A floating point number
+ * @param int $precision The precision of the returned number
+ * @param array $options Options
+ * @return string Percentage string
+ * @link https://book.cakephp.org/4/en/core-libraries/number.html#formatting-percentages
+ */
+ public static function toPercentage($value, int $precision = 2, array $options = []): string
+ {
+ $options += ['multiply' => false, 'type' => NumberFormatter::PERCENT];
+ if (!$options['multiply']) {
+ $value /= 100;
+ }
+
+ return static::precision($value, $precision, $options);
+ }
+
+ /**
+ * Formats a number into the correct locale format
+ *
+ * Options:
+ *
+ * - `places` - Minimum number or decimals to use, e.g 0
+ * - `precision` - Maximum Number of decimal places to use, e.g. 2
+ * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
+ * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
+ * - `before` - The string to place before whole numbers, e.g. '['
+ * - `after` - The string to place after decimal numbers, e.g. ']'
+ *
+ * @param float|string $value A floating point number.
+ * @param array $options An array with options.
+ * @return string Formatted number
+ */
+ public static function format($value, array $options = []): string
+ {
+ $formatter = static::formatter($options);
+ $options += ['before' => '', 'after' => ''];
+
+ return $options['before'] . $formatter->format((float)$value) . $options['after'];
+ }
+
+ /**
+ * Parse a localized numeric string and transform it in a float point
+ *
+ * Options:
+ *
+ * - `locale` - The locale name to use for parsing the number, e.g. fr_FR
+ * - `type` - The formatter type to construct, set it to `currency` if you need to parse
+ * numbers representing money.
+ *
+ * @param string $value A numeric string.
+ * @param array $options An array with options.
+ * @return float point number
+ */
+ public static function parseFloat(string $value, array $options = []): float
+ {
+ $formatter = static::formatter($options);
+
+ return (float)$formatter->parse($value, NumberFormatter::TYPE_DOUBLE);
+ }
+
+ /**
+ * Formats a number into the correct locale format to show deltas (signed differences in value).
+ *
+ * ### Options
+ *
+ * - `places` - Minimum number or decimals to use, e.g 0
+ * - `precision` - Maximum Number of decimal places to use, e.g. 2
+ * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
+ * - `before` - The string to place before whole numbers, e.g. '['
+ * - `after` - The string to place after decimal numbers, e.g. ']'
+ *
+ * @param float|string $value A floating point number
+ * @param array $options Options list.
+ * @return string formatted delta
+ */
+ public static function formatDelta($value, array $options = []): string
+ {
+ $options += ['places' => 0];
+ $value = number_format((float)$value, $options['places'], '.', '');
+ $sign = $value > 0 ? '+' : '';
+ $options['before'] = isset($options['before']) ? $options['before'] . $sign : $sign;
+
+ return static::format($value, $options);
+ }
+
+ /**
+ * Formats a number into a currency format.
+ *
+ * ### Options
+ *
+ * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
+ * - `fractionSymbol` - The currency symbol to use for fractional numbers.
+ * - `fractionPosition` - The position the fraction symbol should be placed
+ * valid options are 'before' & 'after'.
+ * - `before` - Text to display before the rendered number
+ * - `after` - Text to display after the rendered number
+ * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!'
+ * - `places` - Number of decimal places to use. e.g. 2
+ * - `precision` - Maximum Number of decimal places to use, e.g. 2
+ * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
+ * - `useIntlCode` - Whether or not to replace the currency symbol with the international
+ * currency code.
+ *
+ * @param float|string $value Value to format.
+ * @param string|null $currency International currency name such as 'USD', 'EUR', 'JPY', 'CAD'
+ * @param array $options Options list.
+ * @return string Number formatted as a currency.
+ */
+ public static function currency($value, ?string $currency = null, array $options = []): string
+ {
+ $value = (float)$value;
+ $currency = $currency ?: static::getDefaultCurrency();
+
+ if (isset($options['zero']) && !$value) {
+ return $options['zero'];
+ }
+
+ $formatter = static::formatter(['type' => static::getDefaultCurrencyFormat()] + $options);
+ $abs = abs($value);
+ if (!empty($options['fractionSymbol']) && $abs > 0 && $abs < 1) {
+ $value *= 100;
+ $pos = $options['fractionPosition'] ?? 'after';
+
+ return static::format($value, ['precision' => 0, $pos => $options['fractionSymbol']]);
+ }
+
+ $before = $options['before'] ?? '';
+ $after = $options['after'] ?? '';
+ $value = $formatter->formatCurrency($value, $currency);
+
+ return $before . $value . $after;
+ }
+
+ /**
+ * Getter/setter for default currency. This behavior is *deprecated* and will be
+ * removed in future versions of CakePHP.
+ *
+ * @deprecated 3.9.0 Use {@link getDefaultCurrency()} and {@link setDefaultCurrency()} instead.
+ * @param string|false|null $currency Default currency string to be used by {@link currency()}
+ * if $currency argument is not provided. If boolean false is passed, it will clear the
+ * currently stored value
+ * @return string|null Currency
+ */
+ public static function defaultCurrency($currency = null): ?string
+ {
+ deprecationWarning(
+ 'Number::defaultCurrency() is deprecated. ' .
+ 'Use Number::setDefaultCurrency()/getDefaultCurrency() instead.'
+ );
+
+ if ($currency === false) {
+ static::setDefaultCurrency(null);
+
+ // This doesn't seem like a useful result to return, but it's what the old version did.
+ // Retaining it for backward compatibility.
+ return null;
+ }
+ if ($currency !== null) {
+ static::setDefaultCurrency($currency);
+ }
+
+ return static::getDefaultCurrency();
+ }
+
+ /**
+ * Getter for default currency
+ *
+ * @return string Currency
+ */
+ public static function getDefaultCurrency(): string
+ {
+ if (static::$_defaultCurrency === null) {
+ $locale = ini_get('intl.default_locale') ?: static::DEFAULT_LOCALE;
+ $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
+ static::$_defaultCurrency = $formatter->getTextAttribute(NumberFormatter::CURRENCY_CODE);
+ }
+
+ return static::$_defaultCurrency;
+ }
+
+ /**
+ * Setter for default currency
+ *
+ * @param string|null $currency Default currency string to be used by {@link currency()}
+ * if $currency argument is not provided. If null is passed, it will clear the
+ * currently stored value
+ * @return void
+ */
+ public static function setDefaultCurrency(?string $currency = null): void
+ {
+ static::$_defaultCurrency = $currency;
+ }
+
+ /**
+ * Getter for default currency format
+ *
+ * @return string Currency Format
+ */
+ public static function getDefaultCurrencyFormat(): string
+ {
+ if (static::$_defaultCurrencyFormat === null) {
+ static::$_defaultCurrencyFormat = static::FORMAT_CURRENCY;
+ }
+
+ return static::$_defaultCurrencyFormat;
+ }
+
+ /**
+ * Setter for default currency format
+ *
+ * @param string|null $currencyFormat Default currency format to be used by currency()
+ * if $currencyFormat argument is not provided. If null is passed, it will clear the
+ * currently stored value
+ * @return void
+ */
+ public static function setDefaultCurrencyFormat($currencyFormat = null): void
+ {
+ static::$_defaultCurrencyFormat = $currencyFormat;
+ }
+
+ /**
+ * Returns a formatter object that can be reused for similar formatting task
+ * under the same locale and options. This is often a speedier alternative to
+ * using other methods in this class as only one formatter object needs to be
+ * constructed.
+ *
+ * ### Options
+ *
+ * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
+ * - `type` - The formatter type to construct, set it to `currency` if you need to format
+ * numbers representing money or a NumberFormatter constant.
+ * - `places` - Number of decimal places to use. e.g. 2
+ * - `precision` - Maximum Number of decimal places to use, e.g. 2
+ * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
+ * - `useIntlCode` - Whether or not to replace the currency symbol with the international
+ * currency code.
+ *
+ * @param array $options An array with options.
+ * @return \NumberFormatter The configured formatter instance
+ */
+ public static function formatter(array $options = []): NumberFormatter
+ {
+ $locale = $options['locale'] ?? ini_get('intl.default_locale');
+
+ if (!$locale) {
+ $locale = static::DEFAULT_LOCALE;
+ }
+
+ $type = NumberFormatter::DECIMAL;
+ if (!empty($options['type'])) {
+ $type = $options['type'];
+ if ($options['type'] === static::FORMAT_CURRENCY) {
+ $type = NumberFormatter::CURRENCY;
+ } elseif ($options['type'] === static::FORMAT_CURRENCY_ACCOUNTING) {
+ if (defined('NumberFormatter::CURRENCY_ACCOUNTING')) {
+ $type = NumberFormatter::CURRENCY_ACCOUNTING;
+ } else {
+ $type = static::CURRENCY_ACCOUNTING;
+ }
+ }
+ }
+
+ if (!isset(static::$_formatters[$locale][$type])) {
+ static::$_formatters[$locale][$type] = new NumberFormatter($locale, $type);
+ }
+
+ /** @var \NumberFormatter $formatter */
+ $formatter = static::$_formatters[$locale][$type];
+
+ // PHP 8.0.0 - 8.0.6 throws an exception when cloning NumberFormatter after a failed parse
+ if (version_compare(PHP_VERSION, '8.0.6', '>') || version_compare(PHP_VERSION, '8.0.0', '<')) {
+ $options = array_intersect_key($options, [
+ 'places' => null,
+ 'precision' => null,
+ 'pattern' => null,
+ 'useIntlCode' => null,
+ ]);
+ if (empty($options)) {
+ return $formatter;
+ }
+ }
+
+ $formatter = clone $formatter;
+
+ return static::_setAttributes($formatter, $options);
+ }
+
+ /**
+ * Configure formatters.
+ *
+ * @param string $locale The locale name to use for formatting the number, e.g. fr_FR
+ * @param int $type The formatter type to construct. Defaults to NumberFormatter::DECIMAL.
+ * @param array $options See Number::formatter() for possible options.
+ * @return void
+ */
+ public static function config(string $locale, int $type = NumberFormatter::DECIMAL, array $options = []): void
+ {
+ static::$_formatters[$locale][$type] = static::_setAttributes(
+ new NumberFormatter($locale, $type),
+ $options
+ );
+ }
+
+ /**
+ * Set formatter attributes
+ *
+ * @param \NumberFormatter $formatter Number formatter instance.
+ * @param array $options See Number::formatter() for possible options.
+ * @return \NumberFormatter
+ */
+ protected static function _setAttributes(NumberFormatter $formatter, array $options = []): NumberFormatter
+ {
+ if (isset($options['places'])) {
+ $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['places']);
+ }
+
+ if (isset($options['precision'])) {
+ $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $options['precision']);
+ }
+
+ if (!empty($options['pattern'])) {
+ $formatter->setPattern($options['pattern']);
+ }
+
+ if (!empty($options['useIntlCode'])) {
+ // One of the odd things about ICU is that the currency marker in patterns
+ // is denoted with ¤, whereas the international code is marked with ¤¤,
+ // in order to use the code we need to simply duplicate the character wherever
+ // it appears in the pattern.
+ $pattern = trim(str_replace('¤', '¤¤ ', $formatter->getPattern()));
+ $formatter->setPattern($pattern);
+ }
+
+ return $formatter;
+ }
+
+ /**
+ * Returns a formatted integer as an ordinal number string (e.g. 1st, 2nd, 3rd, 4th, [...])
+ *
+ * ### Options
+ *
+ * - `type` - The formatter type to construct, set it to `currency` if you need to format
+ * numbers representing money or a NumberFormatter constant.
+ *
+ * For all other options see formatter().
+ *
+ * @param int|float $value An integer
+ * @param array $options An array with options.
+ * @return string
+ */
+ public static function ordinal($value, array $options = []): string
+ {
+ return static::formatter(['type' => NumberFormatter::ORDINAL] + $options)->format($value);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Package.php b/app/vendor/cakephp/cakephp/src/I18n/Package.php
new file mode 100644
index 000000000..680e13bbd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Package.php
@@ -0,0 +1,164 @@
+
+ */
+ protected $messages = [];
+
+ /**
+ * The name of a fallback package to use when a message key does not
+ * exist.
+ *
+ * @var string|null
+ */
+ protected $fallback;
+
+ /**
+ * The name of the formatter to use when formatting translated messages.
+ *
+ * @var string
+ */
+ protected $formatter;
+
+ /**
+ * Constructor.
+ *
+ * @param string $formatter The name of the formatter to use.
+ * @param string|null $fallback The name of the fallback package to use.
+ * @param array $messages The messages in this package.
+ */
+ public function __construct(
+ string $formatter = 'default',
+ ?string $fallback = null,
+ array $messages = []
+ ) {
+ $this->formatter = $formatter;
+ $this->fallback = $fallback;
+ $this->messages = $messages;
+ }
+
+ /**
+ * Sets the messages for this package.
+ *
+ * @param array $messages The messages for this package.
+ * @return void
+ */
+ public function setMessages(array $messages): void
+ {
+ $this->messages = $messages;
+ }
+
+ /**
+ * Adds one message for this package.
+ *
+ * @param string $key the key of the message
+ * @param string|array $message the actual message
+ * @return void
+ */
+ public function addMessage(string $key, $message): void
+ {
+ $this->messages[$key] = $message;
+ }
+
+ /**
+ * Adds new messages for this package.
+ *
+ * @param array $messages The messages to add in this package.
+ * @return void
+ */
+ public function addMessages(array $messages): void
+ {
+ $this->messages = array_merge($this->messages, $messages);
+ }
+
+ /**
+ * Gets the messages for this package.
+ *
+ * @return array
+ */
+ public function getMessages(): array
+ {
+ return $this->messages;
+ }
+
+ /**
+ * Gets the message of the given key for this package.
+ *
+ * @param string $key the key of the message to return
+ * @return string|array|false The message translation, or false if not found.
+ */
+ public function getMessage(string $key)
+ {
+ if (isset($this->messages[$key])) {
+ return $this->messages[$key];
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the formatter name for this package.
+ *
+ * @param string $formatter The formatter name for this package.
+ * @return void
+ */
+ public function setFormatter(string $formatter): void
+ {
+ $this->formatter = $formatter;
+ }
+
+ /**
+ * Gets the formatter name for this package.
+ *
+ * @return string
+ */
+ public function getFormatter(): string
+ {
+ return $this->formatter;
+ }
+
+ /**
+ * Sets the fallback package name.
+ *
+ * @param string|null $fallback The fallback package name.
+ * @return void
+ */
+ public function setFallback(?string $fallback): void
+ {
+ $this->fallback = $fallback;
+ }
+
+ /**
+ * Gets the fallback package name.
+ *
+ * @return string|null
+ */
+ public function getFallback(): ?string
+ {
+ return $this->fallback;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/PackageLocator.php b/app/vendor/cakephp/cakephp/src/I18n/PackageLocator.php
new file mode 100644
index 000000000..6cc3f8bcf
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/PackageLocator.php
@@ -0,0 +1,110 @@
+ $locales) {
+ foreach ($locales as $locale => $spec) {
+ $this->set($name, $locale, $spec);
+ }
+ }
+ }
+
+ /**
+ * Sets a Package loader.
+ *
+ * @param string $name The package name.
+ * @param string $locale The locale for the package.
+ * @param callable|\Cake\I18n\Package $spec A callable that returns a package or Package instance.
+ * @return void
+ */
+ public function set(string $name, string $locale, $spec): void
+ {
+ $this->registry[$name][$locale] = $spec;
+ $this->converted[$name][$locale] = $spec instanceof Package;
+ }
+
+ /**
+ * Gets a Package object.
+ *
+ * @param string $name The package name.
+ * @param string $locale The locale for the package.
+ * @return \Cake\I18n\Package
+ */
+ public function get(string $name, string $locale): Package
+ {
+ if (!isset($this->registry[$name][$locale])) {
+ throw new I18nException("Package '$name' with locale '$locale' is not registered.");
+ }
+
+ if (!$this->converted[$name][$locale]) {
+ $func = $this->registry[$name][$locale];
+ $this->registry[$name][$locale] = $func();
+ $this->converted[$name][$locale] = true;
+ }
+
+ return $this->registry[$name][$locale];
+ }
+
+ /**
+ * Check if a Package object for given name and locale exists in registry.
+ *
+ * @param string $name The package name.
+ * @param string $locale The locale for the package.
+ * @return bool
+ */
+ public function has(string $name, string $locale): bool
+ {
+ return isset($this->registry[$name][$locale]);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Parser/MoFileParser.php b/app/vendor/cakephp/cakephp/src/I18n/Parser/MoFileParser.php
new file mode 100644
index 000000000..69633363a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Parser/MoFileParser.php
@@ -0,0 +1,162 @@
+_readLong($stream, $isBigEndian);
+ $offsetId = $this->_readLong($stream, $isBigEndian);
+ $offsetTranslated = $this->_readLong($stream, $isBigEndian);
+
+ // Offset to start of translations
+ fread($stream, 8);
+ $messages = [];
+
+ for ($i = 0; $i < $count; $i++) {
+ $pluralId = null;
+ $context = null;
+ $plurals = null;
+
+ fseek($stream, $offsetId + $i * 8);
+
+ $length = $this->_readLong($stream, $isBigEndian);
+ $offset = $this->_readLong($stream, $isBigEndian);
+
+ if ($length < 1) {
+ continue;
+ }
+
+ fseek($stream, $offset);
+ $singularId = fread($stream, $length);
+
+ if (strpos($singularId, "\x04") !== false) {
+ [$context, $singularId] = explode("\x04", $singularId);
+ }
+
+ if (strpos($singularId, "\000") !== false) {
+ [$singularId, $pluralId] = explode("\000", $singularId);
+ }
+
+ fseek($stream, $offsetTranslated + $i * 8);
+ $length = $this->_readLong($stream, $isBigEndian);
+ $offset = $this->_readLong($stream, $isBigEndian);
+ fseek($stream, $offset);
+ $translated = fread($stream, $length);
+
+ if ($pluralId !== null || strpos($translated, "\000") !== false) {
+ $translated = explode("\000", $translated);
+ $plurals = $pluralId !== null ? $translated : null;
+ $translated = $translated[0];
+ }
+
+ $singular = $translated;
+ if ($context !== null) {
+ $messages[$singularId]['_context'][$context] = $singular;
+ if ($pluralId !== null) {
+ $messages[$pluralId]['_context'][$context] = $plurals;
+ }
+ continue;
+ }
+
+ $messages[$singularId]['_context'][''] = $singular;
+ if ($pluralId !== null) {
+ $messages[$pluralId]['_context'][''] = $plurals;
+ }
+ }
+
+ fclose($stream);
+
+ return $messages;
+ }
+
+ /**
+ * Reads an unsigned long from stream respecting endianess.
+ *
+ * @param resource $stream The File being read.
+ * @param bool $isBigEndian Whether or not the current platform is Big Endian
+ * @return int
+ */
+ protected function _readLong($stream, $isBigEndian): int
+ {
+ $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4));
+ $result = current($result);
+
+ return (int)substr((string)$result, -8);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Parser/PoFileParser.php b/app/vendor/cakephp/cakephp/src/I18n/Parser/PoFileParser.php
new file mode 100644
index 000000000..a3b195923
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Parser/PoFileParser.php
@@ -0,0 +1,198 @@
+ [],
+ 'translated' => null,
+ ];
+
+ $messages = [];
+ $item = $defaults;
+ $stage = [];
+
+ while ($line = fgets($stream)) {
+ $line = trim($line);
+
+ if ($line === '') {
+ // Whitespace indicated current item is done
+ $this->_addMessage($messages, $item);
+ $item = $defaults;
+ $stage = [];
+ } elseif (substr($line, 0, 7) === 'msgid "') {
+ // We start a new msg so save previous
+ $this->_addMessage($messages, $item);
+ /** @psalm-suppress InvalidArrayOffset */
+ $item['ids']['singular'] = substr($line, 7, -1);
+ $stage = ['ids', 'singular'];
+ } elseif (substr($line, 0, 8) === 'msgstr "') {
+ $item['translated'] = substr($line, 8, -1);
+ $stage = ['translated'];
+ } elseif (substr($line, 0, 9) === 'msgctxt "') {
+ $item['context'] = substr($line, 9, -1);
+ $stage = ['context'];
+ } elseif ($line[0] === '"') {
+ switch (count($stage)) {
+ case 2:
+ /**
+ * @psalm-suppress PossiblyUndefinedArrayOffset
+ * @psalm-suppress InvalidArrayOffset
+ * @psalm-suppress PossiblyNullArrayAccess
+ */
+ $item[$stage[0]][$stage[1]] .= substr($line, 1, -1);
+ break;
+
+ case 1:
+ /**
+ * @psalm-suppress PossiblyUndefinedArrayOffset
+ * @psalm-suppress PossiblyInvalidOperand
+ * @psalm-suppress PossiblyNullOperand
+ */
+ $item[$stage[0]] .= substr($line, 1, -1);
+ break;
+ }
+ } elseif (substr($line, 0, 14) === 'msgid_plural "') {
+ /** @psalm-suppress InvalidArrayOffset */
+ $item['ids']['plural'] = substr($line, 14, -1);
+ $stage = ['ids', 'plural'];
+ } elseif (substr($line, 0, 7) === 'msgstr[') {
+ /** @var int $size */
+ $size = strpos($line, ']');
+ $row = (int)substr($line, 7, 1);
+ $item['translated'][$row] = substr($line, $size + 3, -1);
+ $stage = ['translated', $row];
+ }
+ }
+ // save last item
+ $this->_addMessage($messages, $item);
+ fclose($stream);
+
+ return $messages;
+ }
+
+ /**
+ * Saves a translation item to the messages.
+ *
+ * @param array $messages The messages array being collected from the file
+ * @param array $item The current item being inspected
+ * @return void
+ */
+ protected function _addMessage(array &$messages, array $item): void
+ {
+ if (empty($item['ids']['singular']) && empty($item['ids']['plural'])) {
+ return;
+ }
+
+ $singular = stripcslashes($item['ids']['singular']);
+ $context = $item['context'] ?? null;
+ $translation = $item['translated'];
+
+ if (is_array($translation)) {
+ $translation = $translation[0];
+ }
+
+ $translation = stripcslashes((string)$translation);
+
+ if ($context !== null && !isset($messages[$singular]['_context'][$context])) {
+ $messages[$singular]['_context'][$context] = $translation;
+ } elseif (!isset($messages[$singular]['_context'][''])) {
+ $messages[$singular]['_context'][''] = $translation;
+ }
+
+ if (isset($item['ids']['plural'])) {
+ $plurals = $item['translated'];
+ // PO are by definition indexed so sort by index.
+ ksort($plurals);
+
+ // Make sure every index is filled.
+ end($plurals);
+ $count = (int)key($plurals);
+
+ // Fill missing spots with an empty string.
+ $empties = array_fill(0, $count + 1, '');
+ $plurals += $empties;
+ ksort($plurals);
+
+ $plurals = array_map('stripcslashes', $plurals);
+ $key = stripcslashes($item['ids']['plural']);
+
+ if ($context !== null) {
+ $messages[Translator::PLURAL_PREFIX . $key]['_context'][$context] = $plurals;
+ } else {
+ $messages[Translator::PLURAL_PREFIX . $key]['_context'][''] = $plurals;
+ }
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/PluralRules.php b/app/vendor/cakephp/cakephp/src/I18n/PluralRules.php
new file mode 100644
index 000000000..b52514fe1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/PluralRules.php
@@ -0,0 +1,205 @@
+ plurals group used to determine
+ * which plural rules apply to the language
+ *
+ * @var array
+ */
+ protected static $_rulesMap = [
+ 'af' => 1,
+ 'am' => 2,
+ 'ar' => 13,
+ 'az' => 1,
+ 'be' => 3,
+ 'bg' => 1,
+ 'bh' => 2,
+ 'bn' => 1,
+ 'bo' => 0,
+ 'bs' => 3,
+ 'ca' => 1,
+ 'cs' => 4,
+ 'cy' => 14,
+ 'da' => 1,
+ 'de' => 1,
+ 'dz' => 0,
+ 'el' => 1,
+ 'en' => 1,
+ 'eo' => 1,
+ 'es' => 1,
+ 'et' => 1,
+ 'eu' => 1,
+ 'fa' => 1,
+ 'fi' => 1,
+ 'fil' => 2,
+ 'fo' => 1,
+ 'fr' => 2,
+ 'fur' => 1,
+ 'fy' => 1,
+ 'ga' => 5,
+ 'gl' => 1,
+ 'gu' => 1,
+ 'gun' => 2,
+ 'ha' => 1,
+ 'he' => 1,
+ 'hi' => 2,
+ 'hr' => 3,
+ 'hu' => 1,
+ 'id' => 0,
+ 'is' => 15,
+ 'it' => 1,
+ 'ja' => 0,
+ 'jv' => 0,
+ 'ka' => 0,
+ 'km' => 0,
+ 'kn' => 0,
+ 'ko' => 0,
+ 'ku' => 1,
+ 'lb' => 1,
+ 'ln' => 2,
+ 'lt' => 6,
+ 'lv' => 10,
+ 'mg' => 2,
+ 'mk' => 8,
+ 'ml' => 1,
+ 'mn' => 1,
+ 'mr' => 1,
+ 'ms' => 0,
+ 'mt' => 9,
+ 'nah' => 1,
+ 'nb' => 1,
+ 'ne' => 1,
+ 'nl' => 1,
+ 'nn' => 1,
+ 'no' => 1,
+ 'nso' => 2,
+ 'om' => 1,
+ 'or' => 1,
+ 'pa' => 1,
+ 'pap' => 1,
+ 'pl' => 11,
+ 'ps' => 1,
+ 'pt_pt' => 2,
+ 'pt' => 1,
+ 'ro' => 12,
+ 'ru' => 3,
+ 'sk' => 4,
+ 'sl' => 7,
+ 'so' => 1,
+ 'sq' => 1,
+ 'sr' => 3,
+ 'sv' => 1,
+ 'sw' => 1,
+ 'ta' => 1,
+ 'te' => 1,
+ 'th' => 0,
+ 'ti' => 2,
+ 'tk' => 1,
+ 'tr' => 1,
+ 'uk' => 3,
+ 'ur' => 1,
+ 'vi' => 0,
+ 'wa' => 2,
+ 'zh' => 0,
+ 'zu' => 1,
+ ];
+
+ /**
+ * Returns the plural form number for the passed locale corresponding
+ * to the countable provided in $n.
+ *
+ * @param string $locale The locale to get the rule calculated for.
+ * @param int $n The number to apply the rules to.
+ * @return int The plural rule number that should be used.
+ * @link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html
+ * @link https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_and_Plurals#List_of_Plural_Rules
+ */
+ public static function calculate(string $locale, $n): int
+ {
+ $locale = strtolower($locale);
+
+ if (!isset(static::$_rulesMap[$locale])) {
+ $locale = explode('_', $locale)[0];
+ }
+
+ if (!isset(static::$_rulesMap[$locale])) {
+ return 0;
+ }
+
+ switch (static::$_rulesMap[$locale]) {
+ case 0:
+ return 0;
+ case 1:
+ return $n === 1 ? 0 : 1;
+ case 2:
+ return $n > 1 ? 1 : 0;
+ case 3:
+ return $n % 10 === 1 && $n % 100 !== 11 ? 0 :
+ (($n % 10 >= 2 && $n % 10 <= 4) && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
+ case 4:
+ return $n === 1 ? 0 :
+ ($n >= 2 && $n <= 4 ? 1 : 2);
+ case 5:
+ return $n === 1 ? 0 :
+ ($n === 2 ? 1 : ($n < 7 ? 2 : ($n < 11 ? 3 : 4)));
+ case 6:
+ return $n % 10 === 1 && $n % 100 !== 11 ? 0 :
+ ($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
+ case 7:
+ return $n % 100 === 1 ? 1 :
+ ($n % 100 === 2 ? 2 : ($n % 100 === 3 || $n % 100 === 4 ? 3 : 0));
+ case 8:
+ return $n % 10 === 1 ? 0 : ($n % 10 === 2 ? 1 : 2);
+ case 9:
+ return $n === 1 ? 0 :
+ ($n === 0 || ($n % 100 > 0 && $n % 100 <= 10) ? 1 :
+ ($n % 100 > 10 && $n % 100 < 20 ? 2 : 3));
+ case 10:
+ return $n % 10 === 1 && $n % 100 !== 11 ? 0 : ($n !== 0 ? 1 : 2);
+ case 11:
+ return $n === 1 ? 0 :
+ ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
+ case 12:
+ return $n === 1 ? 0 :
+ ($n === 0 || $n % 100 > 0 && $n % 100 < 20 ? 1 : 2);
+ case 13:
+ return $n === 0 ? 0 :
+ ($n === 1 ? 1 :
+ ($n === 2 ? 2 :
+ ($n % 100 >= 3 && $n % 100 <= 10 ? 3 :
+ ($n % 100 >= 11 ? 4 : 5))));
+ case 14:
+ return $n === 1 ? 0 :
+ ($n === 2 ? 1 :
+ ($n !== 8 && $n !== 11 ? 2 : 3));
+ case 15:
+ return $n % 10 !== 1 || $n % 100 === 11 ? 1 : 0;
+ }
+
+ throw new CakeException('Unable to find plural rule number.');
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/README.md b/app/vendor/cakephp/cakephp/src/I18n/README.md
new file mode 100644
index 000000000..e7724b8fe
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/README.md
@@ -0,0 +1,103 @@
+[](https://packagist.org/packages/cakephp/i18n)
+[](LICENSE.txt)
+
+# CakePHP Internationalization Library
+
+The I18n library provides a `I18n` service locator that can be used for setting
+the current locale, building translation bundles and translating messages.
+
+Additionally, it provides the `Time` and `Number` classes which can be used to
+output dates, currencies and any numbers in the right format for the specified locale.
+
+## Usage
+
+Internally, the `I18n` class uses [Aura.Intl](https://github.com/auraphp/Aura.Intl).
+Getting familiar with it will help you understand how to build and manipulate translation bundles,
+should you wish to create them manually instead of using the conventions this library uses.
+
+### Setting the Current Locale
+
+```php
+use Cake\I18n\I18n;
+
+I18n::setLocale('en_US');
+```
+
+### Setting path to folder containing po files.
+
+```php
+use Cake\Core\Configure;
+
+Configure::write('App.paths.locales', ['/path/with/trailing/slash/']);
+```
+
+Please refer to the [CakePHP Manual](https://book.cakephp.org/4/en/core-libraries/internationalization-and-localization.html#language-files) for details
+about expected folder structure and file naming.
+
+### Translating a Message
+
+```php
+echo __(
+ 'Hi {0,string}, your balance on the {1,date} is {2,number,currency}',
+ ['Charles', '2014-01-13 11:12:00', 1354.37]
+);
+
+// Returns
+Hi Charles, your balance on the Jan 13, 2014, 11:12 AM is $ 1,354.37
+```
+
+### Creating Your Own Translators
+
+```php
+use Cake\I18n\I18n;
+use Cake\I18n\Package;
+
+I18n::translator('animals', 'fr_FR', function () {
+ $package = new Package(
+ 'default', // The formatting strategy (ICU)
+ 'default', // The fallback domain
+ );
+ $package->setMessages([
+ 'Dog' => 'Chien',
+ 'Cat' => 'Chat',
+ 'Bird' => 'Oiseau'
+ ...
+ ]);
+
+ return $package;
+});
+
+I18n::getLocale('fr_FR');
+__d('animals', 'Dog'); // Returns "Chien"
+```
+
+### Formatting Time
+
+```php
+$time = Time::now();
+echo $time; // shows '4/20/14, 10:10 PM' for the en-US locale
+```
+
+### Formatting Numbers
+
+```php
+echo Number::format(100100100);
+```
+
+```php
+echo Number::currency(123456.7890, 'EUR');
+// outputs €123,456.79
+```
+
+## Documentation
+
+Please make sure you check the [official I18n
+documentation](https://book.cakephp.org/4/en/core-libraries/internationalization-and-localization.html).
+
+The [documentation for the Time
+class](https://book.cakephp.org/4/en/core-libraries/time.html) contains
+instructions on how to configure and output time strings for selected locales.
+
+The [documentation for the Number
+class](https://book.cakephp.org/4/en/core-libraries/number.html) shows how to
+use the `Number` class for displaying numbers in specific locales.
diff --git a/app/vendor/cakephp/cakephp/src/I18n/RelativeTimeFormatter.php b/app/vendor/cakephp/cakephp/src/I18n/RelativeTimeFormatter.php
new file mode 100644
index 000000000..0a1f49f0f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/RelativeTimeFormatter.php
@@ -0,0 +1,424 @@
+now($date->getTimezone());
+ }
+ /** @psalm-suppress PossiblyNullArgument */
+ $diffInterval = $date->diff($other);
+
+ switch (true) {
+ case $diffInterval->y > 0:
+ $count = $diffInterval->y;
+ $message = __dn('cake', '{0} year', '{0} years', $count, $count);
+ break;
+ case $diffInterval->m > 0:
+ $count = $diffInterval->m;
+ $message = __dn('cake', '{0} month', '{0} months', $count, $count);
+ break;
+ case $diffInterval->d > 0:
+ $count = $diffInterval->d;
+ if ($count >= I18nDateTimeInterface::DAYS_PER_WEEK) {
+ $count = (int)($count / I18nDateTimeInterface::DAYS_PER_WEEK);
+ $message = __dn('cake', '{0} week', '{0} weeks', $count, $count);
+ } else {
+ $message = __dn('cake', '{0} day', '{0} days', $count, $count);
+ }
+ break;
+ case $diffInterval->h > 0:
+ $count = $diffInterval->h;
+ $message = __dn('cake', '{0} hour', '{0} hours', $count, $count);
+ break;
+ case $diffInterval->i > 0:
+ $count = $diffInterval->i;
+ $message = __dn('cake', '{0} minute', '{0} minutes', $count, $count);
+ break;
+ default:
+ $count = $diffInterval->s;
+ $message = __dn('cake', '{0} second', '{0} seconds', $count, $count);
+ break;
+ }
+ if ($absolute) {
+ return $message;
+ }
+ $isFuture = $diffInterval->invert === 1;
+ if ($isNow) {
+ return $isFuture ? __d('cake', '{0} from now', $message) : __d('cake', '{0} ago', $message);
+ }
+
+ return $isFuture ? __d('cake', '{0} after', $message) : __d('cake', '{0} before', $message);
+ }
+
+ /**
+ * Format a into a relative timestring.
+ *
+ * @param \Cake\I18n\I18nDateTimeInterface $time The time instance to format.
+ * @param array $options Array of options.
+ * @return string Relative time string.
+ * @see \Cake\I18n\Time::timeAgoInWords()
+ */
+ public function timeAgoInWords(I18nDateTimeInterface $time, array $options = []): string
+ {
+ $options = $this->_options($options, FrozenTime::class);
+ if ($options['timezone']) {
+ $time = $time->timezone($options['timezone']);
+ }
+
+ $now = $options['from']->format('U');
+ $inSeconds = $time->format('U');
+ $backwards = ($inSeconds > $now);
+
+ $futureTime = $now;
+ $pastTime = $inSeconds;
+ if ($backwards) {
+ $futureTime = $inSeconds;
+ $pastTime = $now;
+ }
+ $diff = $futureTime - $pastTime;
+
+ if (!$diff) {
+ return __d('cake', 'just now', 'just now');
+ }
+
+ if ($diff > abs($now - (new FrozenTime($options['end']))->format('U'))) {
+ return sprintf($options['absoluteString'], $time->i18nFormat($options['format']));
+ }
+
+ $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options);
+ [$fNum, $fWord, $years, $months, $weeks, $days, $hours, $minutes, $seconds] = array_values($diffData);
+
+ $relativeDate = [];
+ if ($fNum >= 1 && $years > 0) {
+ $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years);
+ }
+ if ($fNum >= 2 && $months > 0) {
+ $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months);
+ }
+ if ($fNum >= 3 && $weeks > 0) {
+ $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks);
+ }
+ if ($fNum >= 4 && $days > 0) {
+ $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days);
+ }
+ if ($fNum >= 5 && $hours > 0) {
+ $relativeDate[] = __dn('cake', '{0} hour', '{0} hours', $hours, $hours);
+ }
+ if ($fNum >= 6 && $minutes > 0) {
+ $relativeDate[] = __dn('cake', '{0} minute', '{0} minutes', $minutes, $minutes);
+ }
+ if ($fNum >= 7 && $seconds > 0) {
+ $relativeDate[] = __dn('cake', '{0} second', '{0} seconds', $seconds, $seconds);
+ }
+ $relativeDate = implode(', ', $relativeDate);
+
+ // When time has passed
+ if (!$backwards) {
+ $aboutAgo = [
+ 'second' => __d('cake', 'about a second ago'),
+ 'minute' => __d('cake', 'about a minute ago'),
+ 'hour' => __d('cake', 'about an hour ago'),
+ 'day' => __d('cake', 'about a day ago'),
+ 'week' => __d('cake', 'about a week ago'),
+ 'month' => __d('cake', 'about a month ago'),
+ 'year' => __d('cake', 'about a year ago'),
+ ];
+
+ return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord];
+ }
+
+ // When time is to come
+ if ($relativeDate) {
+ return $relativeDate;
+ }
+ $aboutIn = [
+ 'second' => __d('cake', 'in about a second'),
+ 'minute' => __d('cake', 'in about a minute'),
+ 'hour' => __d('cake', 'in about an hour'),
+ 'day' => __d('cake', 'in about a day'),
+ 'week' => __d('cake', 'in about a week'),
+ 'month' => __d('cake', 'in about a month'),
+ 'year' => __d('cake', 'in about a year'),
+ ];
+
+ return $aboutIn[$fWord];
+ }
+
+ /**
+ * Calculate the data needed to format a relative difference string.
+ *
+ * @param int|string $futureTime The timestamp from the future.
+ * @param int|string $pastTime The timestamp from the past.
+ * @param bool $backwards Whether or not the difference was backwards.
+ * @param array $options An array of options.
+ * @return array An array of values.
+ */
+ protected function _diffData($futureTime, $pastTime, bool $backwards, $options): array
+ {
+ $futureTime = (int)$futureTime;
+ $pastTime = (int)$pastTime;
+ $diff = $futureTime - $pastTime;
+
+ // If more than a week, then take into account the length of months
+ if ($diff >= 604800) {
+ $future = [];
+ [
+ $future['H'],
+ $future['i'],
+ $future['s'],
+ $future['d'],
+ $future['m'],
+ $future['Y'],
+ ] = explode('/', date('H/i/s/d/m/Y', $futureTime));
+
+ $past = [];
+ [
+ $past['H'],
+ $past['i'],
+ $past['s'],
+ $past['d'],
+ $past['m'],
+ $past['Y'],
+ ] = explode('/', date('H/i/s/d/m/Y', $pastTime));
+ $weeks = $days = $hours = $minutes = $seconds = 0;
+
+ $years = (int)$future['Y'] - (int)$past['Y'];
+ $months = (int)$future['m'] + (12 * $years) - (int)$past['m'];
+
+ if ($months >= 12) {
+ $years = floor($months / 12);
+ $months -= $years * 12;
+ }
+ if ((int)$future['m'] < (int)$past['m'] && (int)$future['Y'] - (int)$past['Y'] === 1) {
+ $years--;
+ }
+
+ if ((int)$future['d'] >= (int)$past['d']) {
+ $days = (int)$future['d'] - (int)$past['d'];
+ } else {
+ $daysInPastMonth = (int)date('t', $pastTime);
+ $daysInFutureMonth = (int)date('t', mktime(0, 0, 0, (int)$future['m'] - 1, 1, (int)$future['Y']));
+
+ if (!$backwards) {
+ $days = $daysInPastMonth - (int)$past['d'] + (int)$future['d'];
+ } else {
+ $days = $daysInFutureMonth - (int)$past['d'] + (int)$future['d'];
+ }
+
+ if ($future['m'] !== $past['m']) {
+ $months--;
+ }
+ }
+
+ if (!$months && $years >= 1 && $diff < $years * 31536000) {
+ $months = 11;
+ $years--;
+ }
+
+ if ($months >= 12) {
+ $years++;
+ $months -= 12;
+ }
+
+ if ($days >= 7) {
+ $weeks = floor($days / 7);
+ $days -= $weeks * 7;
+ }
+ } else {
+ $years = $months = $weeks = 0;
+ $days = floor($diff / 86400);
+
+ $diff -= $days * 86400;
+
+ $hours = floor($diff / 3600);
+ $diff -= $hours * 3600;
+
+ $minutes = floor($diff / 60);
+ $diff -= $minutes * 60;
+ $seconds = $diff;
+ }
+
+ $fWord = $options['accuracy']['second'];
+ if ($years > 0) {
+ $fWord = $options['accuracy']['year'];
+ } elseif (abs($months) > 0) {
+ $fWord = $options['accuracy']['month'];
+ } elseif (abs($weeks) > 0) {
+ $fWord = $options['accuracy']['week'];
+ } elseif (abs($days) > 0) {
+ $fWord = $options['accuracy']['day'];
+ } elseif (abs($hours) > 0) {
+ $fWord = $options['accuracy']['hour'];
+ } elseif (abs($minutes) > 0) {
+ $fWord = $options['accuracy']['minute'];
+ }
+
+ $fNum = str_replace(
+ ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'],
+ ['1', '2', '3', '4', '5', '6', '7'],
+ $fWord
+ );
+
+ return [
+ $fNum,
+ $fWord,
+ (int)$years,
+ (int)$months,
+ (int)$weeks,
+ (int)$days,
+ (int)$hours,
+ (int)$minutes,
+ (int)$seconds,
+ ];
+ }
+
+ /**
+ * Format a into a relative date string.
+ *
+ * @param \Cake\I18n\I18nDateTimeInterface $date The date to format.
+ * @param array $options Array of options.
+ * @return string Relative date string.
+ * @see \Cake\I18n\Date::timeAgoInWords()
+ */
+ public function dateAgoInWords(I18nDateTimeInterface $date, array $options = []): string
+ {
+ $options = $this->_options($options, FrozenDate::class);
+ if ($options['timezone']) {
+ $date = $date->timezone($options['timezone']);
+ }
+
+ $now = $options['from']->format('U');
+ $inSeconds = $date->format('U');
+ $backwards = ($inSeconds > $now);
+
+ $futureTime = $now;
+ $pastTime = $inSeconds;
+ if ($backwards) {
+ $futureTime = $inSeconds;
+ $pastTime = $now;
+ }
+ $diff = $futureTime - $pastTime;
+
+ if (!$diff) {
+ return __d('cake', 'today');
+ }
+
+ if ($diff > abs($now - (new FrozenDate($options['end']))->format('U'))) {
+ return sprintf($options['absoluteString'], $date->i18nFormat($options['format']));
+ }
+
+ $diffData = $this->_diffData($futureTime, $pastTime, $backwards, $options);
+ [$fNum, $fWord, $years, $months, $weeks, $days] = array_values($diffData);
+
+ $relativeDate = [];
+ if ($fNum >= 1 && $years > 0) {
+ $relativeDate[] = __dn('cake', '{0} year', '{0} years', $years, $years);
+ }
+ if ($fNum >= 2 && $months > 0) {
+ $relativeDate[] = __dn('cake', '{0} month', '{0} months', $months, $months);
+ }
+ if ($fNum >= 3 && $weeks > 0) {
+ $relativeDate[] = __dn('cake', '{0} week', '{0} weeks', $weeks, $weeks);
+ }
+ if ($fNum >= 4 && $days > 0) {
+ $relativeDate[] = __dn('cake', '{0} day', '{0} days', $days, $days);
+ }
+ $relativeDate = implode(', ', $relativeDate);
+
+ // When time has passed
+ if (!$backwards) {
+ $aboutAgo = [
+ 'day' => __d('cake', 'about a day ago'),
+ 'week' => __d('cake', 'about a week ago'),
+ 'month' => __d('cake', 'about a month ago'),
+ 'year' => __d('cake', 'about a year ago'),
+ ];
+
+ return $relativeDate ? sprintf($options['relativeString'], $relativeDate) : $aboutAgo[$fWord];
+ }
+
+ // When time is to come
+ if ($relativeDate) {
+ return $relativeDate;
+ }
+ $aboutIn = [
+ 'day' => __d('cake', 'in about a day'),
+ 'week' => __d('cake', 'in about a week'),
+ 'month' => __d('cake', 'in about a month'),
+ 'year' => __d('cake', 'in about a year'),
+ ];
+
+ return $aboutIn[$fWord];
+ }
+
+ /**
+ * Build the options for relative date formatting.
+ *
+ * @param array $options The options provided by the user.
+ * @param string $class The class name to use for defaults.
+ * @return array Options with defaults applied.
+ * @psalm-param class-string<\Cake\I18n\FrozenDate>|class-string<\Cake\I18n\FrozenTime> $class
+ */
+ protected function _options(array $options, string $class): array
+ {
+ $options += [
+ 'from' => $class::now(),
+ 'timezone' => null,
+ 'format' => $class::$wordFormat,
+ 'accuracy' => $class::$wordAccuracy,
+ 'end' => $class::$wordEnd,
+ 'relativeString' => __d('cake', '%s ago'),
+ 'absoluteString' => __d('cake', 'on %s'),
+ ];
+ if (is_string($options['accuracy'])) {
+ $accuracy = $options['accuracy'];
+ $options['accuracy'] = [];
+ foreach ($class::$wordAccuracy as $key => $level) {
+ $options['accuracy'][$key] = $accuracy;
+ }
+ } else {
+ $options['accuracy'] += $class::$wordAccuracy;
+ }
+
+ return $options;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Time.php b/app/vendor/cakephp/cakephp/src/I18n/Time.php
new file mode 100644
index 000000000..fdafc256e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Time.php
@@ -0,0 +1,343 @@
+ 'day',
+ 'month' => 'day',
+ 'week' => 'day',
+ 'day' => 'hour',
+ 'hour' => 'minute',
+ 'minute' => 'minute',
+ 'second' => 'second',
+ ];
+
+ /**
+ * The end of relative time telling
+ *
+ * @var string
+ * @see \Cake\I18n\Time::timeAgoInWords()
+ */
+ public static $wordEnd = '+1 month';
+
+ /**
+ * serialise the value as a Unix Timestamp
+ *
+ * @var string
+ */
+ public const UNIX_TIMESTAMP_FORMAT = 'unixTimestampFormat';
+
+ /**
+ * Create a new mutable time instance.
+ *
+ * @param string|int|\DateTimeInterface|null $time Fixed or relative time
+ * @param \DateTimeZone|string|null $tz The timezone for the instance
+ */
+ public function __construct($time = null, $tz = null)
+ {
+ if ($time instanceof DateTimeInterface) {
+ $tz = $time->getTimezone();
+ $time = $time->format('Y-m-d H:i:s.u');
+ }
+
+ if (is_numeric($time)) {
+ $time = '@' . $time;
+ }
+ parent::__construct($time, $tz);
+ }
+
+ /**
+ * Returns a nicely formatted date string for this object.
+ *
+ * The format to be used is stored in the static property `Time::niceFormat`.
+ *
+ * @param string|\DateTimeZone|null $timezone Timezone string or DateTimeZone object
+ * in which the date will be displayed. The timezone stored for this object will not
+ * be changed.
+ * @param string|null $locale The locale name in which the date should be displayed (e.g. pt-BR)
+ * @return string Formatted date string
+ */
+ public function nice($timezone = null, $locale = null): string
+ {
+ return (string)$this->i18nFormat(static::$niceFormat, $timezone, $locale);
+ }
+
+ /**
+ * Returns true if this object represents a date within the current week
+ *
+ * @return bool
+ */
+ public function isThisWeek(): bool
+ {
+ return static::now($this->getTimezone())->format('W o') === $this->format('W o');
+ }
+
+ /**
+ * Returns true if this object represents a date within the current month
+ *
+ * @return bool
+ */
+ public function isThisMonth(): bool
+ {
+ return static::now($this->getTimezone())->format('m Y') === $this->format('m Y');
+ }
+
+ /**
+ * Returns true if this object represents a date within the current year
+ *
+ * @return bool
+ */
+ public function isThisYear(): bool
+ {
+ return static::now($this->getTimezone())->format('Y') === $this->format('Y');
+ }
+
+ /**
+ * Returns the quarter
+ *
+ * @param bool $range Range.
+ * @return string[]|int 1, 2, 3, or 4 quarter of year, or array if $range true
+ */
+ public function toQuarter(bool $range = false)
+ {
+ $quarter = (int)ceil((int)$this->format('m') / 3);
+ if ($range === false) {
+ return $quarter;
+ }
+
+ $year = $this->format('Y');
+ switch ($quarter) {
+ case 1:
+ return [$year . '-01-01', $year . '-03-31'];
+ case 2:
+ return [$year . '-04-01', $year . '-06-30'];
+ case 3:
+ return [$year . '-07-01', $year . '-09-30'];
+ }
+
+ // 4th quarter
+ return [$year . '-10-01', $year . '-12-31'];
+ }
+
+ /**
+ * Returns a UNIX timestamp.
+ *
+ * @return string UNIX timestamp
+ */
+ public function toUnixString(): string
+ {
+ return $this->format('U');
+ }
+
+ /**
+ * Returns either a relative or a formatted absolute date depending
+ * on the difference between the current time and this object.
+ *
+ * ### Options:
+ *
+ * - `from` => another Time object representing the "now" time
+ * - `format` => a fall back format if the relative time is longer than the duration specified by end
+ * - `accuracy` => Specifies how accurate the date should be described (array)
+ * - year => The format if years > 0 (default "day")
+ * - month => The format if months > 0 (default "day")
+ * - week => The format if weeks > 0 (default "day")
+ * - day => The format if weeks > 0 (default "hour")
+ * - hour => The format if hours > 0 (default "minute")
+ * - minute => The format if minutes > 0 (default "minute")
+ * - second => The format if seconds > 0 (default "second")
+ * - `end` => The end of relative time telling
+ * - `relativeString` => The `printf` compatible string when outputting relative time
+ * - `absoluteString` => The `printf` compatible string when outputting absolute time
+ * - `timezone` => The user timezone the timestamp should be formatted in.
+ *
+ * Relative dates look something like this:
+ *
+ * - 3 weeks, 4 days ago
+ * - 15 seconds ago
+ *
+ * Default date formatting is d/M/YY e.g: on 18/2/09. Formatting is done internally using
+ * `i18nFormat`, see the method for the valid formatting strings
+ *
+ * The returned string includes 'ago' or 'on' and assumes you'll properly add a word
+ * like 'Posted ' before the function output.
+ *
+ * NOTE: If the difference is one week or more, the lowest level of accuracy is day
+ *
+ * @param array $options Array of options.
+ * @return string Relative time string.
+ */
+ public function timeAgoInWords(array $options = []): string
+ {
+ /** @psalm-suppress UndefinedInterfaceMethod */
+ return static::getDiffFormatter()->timeAgoInWords($this, $options);
+ }
+
+ /**
+ * Get list of timezone identifiers
+ *
+ * @param int|string|null $filter A regex to filter identifier
+ * Or one of DateTimeZone class constants
+ * @param string|null $country A two-letter ISO 3166-1 compatible country code.
+ * This option is only used when $filter is set to DateTimeZone::PER_COUNTRY
+ * @param bool|array $options If true (default value) groups the identifiers list by primary region.
+ * Otherwise, an array containing `group`, `abbr`, `before`, and `after`
+ * keys. Setting `group` and `abbr` to true will group results and append
+ * timezone abbreviation in the display value. Set `before` and `after`
+ * to customize the abbreviation wrapper.
+ * @return array List of timezone identifiers
+ * @since 2.2
+ */
+ public static function listTimezones($filter = null, ?string $country = null, $options = []): array
+ {
+ if (is_bool($options)) {
+ $options = [
+ 'group' => $options,
+ ];
+ }
+ $defaults = [
+ 'group' => true,
+ 'abbr' => false,
+ 'before' => ' - ',
+ 'after' => null,
+ ];
+ $options += $defaults;
+ $group = $options['group'];
+
+ $regex = null;
+ if (is_string($filter)) {
+ $regex = $filter;
+ $filter = null;
+ }
+ if ($filter === null) {
+ $filter = DateTimeZone::ALL;
+ }
+ $identifiers = DateTimeZone::listIdentifiers($filter, (string)$country) ?: [];
+
+ if ($regex) {
+ foreach ($identifiers as $key => $tz) {
+ if (!preg_match($regex, $tz)) {
+ unset($identifiers[$key]);
+ }
+ }
+ }
+
+ if ($group) {
+ $groupedIdentifiers = [];
+ $now = time();
+ $before = $options['before'];
+ $after = $options['after'];
+ foreach ($identifiers as $tz) {
+ $abbr = '';
+ if ($options['abbr']) {
+ $dateTimeZone = new DateTimeZone($tz);
+ $trans = $dateTimeZone->getTransitions($now, $now);
+ $abbr = isset($trans[0]['abbr']) ?
+ $before . $trans[0]['abbr'] . $after :
+ '';
+ }
+ $item = explode('/', $tz, 2);
+ if (isset($item[1])) {
+ $groupedIdentifiers[$item[0]][$tz] = $item[1] . $abbr;
+ } else {
+ $groupedIdentifiers[$item[0]] = [$tz => $item[0] . $abbr];
+ }
+ }
+
+ return $groupedIdentifiers;
+ }
+
+ return array_combine($identifiers, $identifiers);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/Translator.php b/app/vendor/cakephp/cakephp/src/I18n/Translator.php
new file mode 100644
index 000000000..58f771fbc
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/Translator.php
@@ -0,0 +1,206 @@
+locale = $locale;
+ $this->package = $package;
+ $this->formatter = $formatter;
+ $this->fallback = $fallback;
+ }
+
+ /**
+ * Gets the message translation by its key.
+ *
+ * @param string $key The message key.
+ * @return mixed The message translation string, or false if not found.
+ */
+ protected function getMessage(string $key)
+ {
+ $message = $this->package->getMessage($key);
+ if ($message) {
+ return $message;
+ }
+
+ if ($this->fallback) {
+ $message = $this->fallback->getMessage($key);
+ if ($message) {
+ $this->package->addMessage($key, $message);
+
+ return $message;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Translates the message formatting any placeholders
+ *
+ * @param string $key The message key.
+ * @param array $tokensValues Token values to interpolate into the
+ * message.
+ * @return string The translated message with tokens replaced.
+ */
+ public function translate(string $key, array $tokensValues = []): string
+ {
+ if (isset($tokensValues['_count'])) {
+ $message = $this->getMessage(static::PLURAL_PREFIX . $key);
+ if (!$message) {
+ $message = $this->getMessage($key);
+ }
+ } else {
+ $message = $this->getMessage($key);
+ if (!$message) {
+ $message = $this->getMessage(static::PLURAL_PREFIX . $key);
+ }
+ }
+
+ if (!$message) {
+ // Fallback to the message key
+ $message = $key;
+ }
+
+ // Check for missing/invalid context
+ if (is_array($message) && isset($message['_context'])) {
+ $message = $this->resolveContext($key, $message, $tokensValues);
+ unset($tokensValues['_context']);
+ }
+
+ if (empty($tokensValues)) {
+ // Fallback for plurals that were using the singular key
+ if (is_array($message)) {
+ return array_values($message + [''])[0];
+ }
+
+ return $message;
+ }
+
+ // Singular message, but plural call
+ if (is_string($message) && isset($tokensValues['_singular'])) {
+ $message = [$tokensValues['_singular'], $message];
+ }
+
+ // Resolve plural form.
+ if (is_array($message)) {
+ $count = $tokensValues['_count'] ?? 0;
+ $form = PluralRules::calculate($this->locale, (int)$count);
+ $message = $message[$form] ?? (string)end($message);
+ }
+
+ if (strlen($message) === 0) {
+ $message = $key;
+ }
+
+ unset($tokensValues['_count'], $tokensValues['_singular']);
+
+ return $this->formatter->format($this->locale, $message, $tokensValues);
+ }
+
+ /**
+ * Resolve a message's context structure.
+ *
+ * @param string $key The message key being handled.
+ * @param array $message The message content.
+ * @param array $vars The variables containing the `_context` key.
+ * @return string|array
+ */
+ protected function resolveContext(string $key, array $message, array $vars)
+ {
+ $context = $vars['_context'] ?? null;
+
+ // No or missing context, fallback to the key/first message
+ if ($context === null) {
+ if (isset($message['_context'][''])) {
+ return $message['_context'][''] === '' ? $key : $message['_context'][''];
+ }
+
+ return current($message['_context']);
+ }
+ if (!isset($message['_context'][$context])) {
+ return $key;
+ }
+ if ($message['_context'][$context] === '') {
+ return $key;
+ }
+
+ return $message['_context'][$context];
+ }
+
+ /**
+ * Returns the translator package
+ *
+ * @return \Cake\I18n\Package
+ */
+ public function getPackage(): Package
+ {
+ return $this->package;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/TranslatorRegistry.php b/app/vendor/cakephp/cakephp/src/I18n/TranslatorRegistry.php
new file mode 100644
index 000000000..181dde02b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/TranslatorRegistry.php
@@ -0,0 +1,351 @@
+>
+ */
+ protected $registry = [];
+
+ /**
+ * The current locale code.
+ *
+ * @var string
+ */
+ protected $locale;
+
+ /**
+ * A package locator.
+ *
+ * @var \Cake\I18n\PackageLocator
+ */
+ protected $packages;
+
+ /**
+ * A formatter locator.
+ *
+ * @var \Cake\I18n\FormatterLocator
+ */
+ protected $formatters;
+
+ /**
+ * A list of loader functions indexed by domain name. Loaders are
+ * callables that are invoked as a default for building translation
+ * packages where none can be found for the combination of translator
+ * name and locale.
+ *
+ * @var callable[]
+ */
+ protected $_loaders = [];
+
+ /**
+ * The name of the default formatter to use for newly created
+ * translators from the fallback loader
+ *
+ * @var string
+ */
+ protected $_defaultFormatter = 'default';
+
+ /**
+ * Use fallback-domain for translation loaders.
+ *
+ * @var bool
+ */
+ protected $_useFallback = true;
+
+ /**
+ * A CacheEngine object that is used to remember translator across
+ * requests.
+ *
+ * @var (\Psr\SimpleCache\CacheInterface&\Cake\Cache\CacheEngineInterface)|null
+ */
+ protected $_cacher;
+
+ /**
+ * Constructor.
+ *
+ * @param \Cake\I18n\PackageLocator $packages The package locator.
+ * @param \Cake\I18n\FormatterLocator $formatters The formatter locator.
+ * @param string $locale The default locale code to use.
+ */
+ public function __construct(
+ PackageLocator $packages,
+ FormatterLocator $formatters,
+ string $locale
+ ) {
+ $this->packages = $packages;
+ $this->formatters = $formatters;
+ $this->setLocale($locale);
+
+ $this->registerLoader(static::FALLBACK_LOADER, function ($name, $locale) {
+ $loader = new ChainMessagesLoader([
+ new MessagesFileLoader($name, $locale, 'mo'),
+ new MessagesFileLoader($name, $locale, 'po'),
+ ]);
+
+ $formatter = $name === 'cake' ? 'default' : $this->_defaultFormatter;
+ $package = $loader();
+ $package->setFormatter($formatter);
+
+ return $package;
+ });
+ }
+
+ /**
+ * Sets the default locale code.
+ *
+ * @param string $locale The new locale code.
+ * @return void
+ */
+ public function setLocale(string $locale): void
+ {
+ $this->locale = $locale;
+ }
+
+ /**
+ * Returns the default locale code.
+ *
+ * @return string
+ */
+ public function getLocale(): string
+ {
+ return $this->locale;
+ }
+
+ /**
+ * Returns the translator packages
+ *
+ * @return \Cake\I18n\PackageLocator
+ */
+ public function getPackages(): PackageLocator
+ {
+ return $this->packages;
+ }
+
+ /**
+ * An object of type FormatterLocator
+ *
+ * @return \Cake\I18n\FormatterLocator
+ */
+ public function getFormatters(): FormatterLocator
+ {
+ return $this->formatters;
+ }
+
+ /**
+ * Sets the CacheEngine instance used to remember translators across
+ * requests.
+ *
+ * @param \Psr\SimpleCache\CacheInterface&\Cake\Cache\CacheEngineInterface $cacher The cacher instance.
+ * @return void
+ */
+ public function setCacher($cacher): void
+ {
+ $this->_cacher = $cacher;
+ }
+
+ /**
+ * Gets a translator from the registry by package for a locale.
+ *
+ * @param string $name The translator package to retrieve.
+ * @param string|null $locale The locale to use; if empty, uses the default
+ * locale.
+ * @return \Cake\I18n\Translator|null A translator object.
+ * @throws \Cake\I18n\Exception\I18nException If no translator with that name could be found
+ * for the given locale.
+ */
+ public function get(string $name, ?string $locale = null): ?Translator
+ {
+ if ($locale === null) {
+ $locale = $this->getLocale();
+ }
+
+ if (isset($this->registry[$name][$locale])) {
+ return $this->registry[$name][$locale];
+ }
+
+ if ($this->_cacher === null) {
+ return $this->registry[$name][$locale] = $this->_getTranslator($name, $locale);
+ }
+
+ // Cache keys cannot contain / if they go to file engine.
+ $keyName = str_replace('/', '.', $name);
+ $key = "translations.{$keyName}.{$locale}";
+ $translator = $this->_cacher->get($key);
+ if (!$translator || !$translator->getPackage()) {
+ $translator = $this->_getTranslator($name, $locale);
+ $this->_cacher->set($key, $translator);
+ }
+
+ return $this->registry[$name][$locale] = $translator;
+ }
+
+ /**
+ * Gets a translator from the registry by package for a locale.
+ *
+ * @param string $name The translator package to retrieve.
+ * @param string $locale The locale to use; if empty, uses the default
+ * locale.
+ * @return \Cake\I18n\Translator A translator object.
+ */
+ protected function _getTranslator(string $name, string $locale): Translator
+ {
+ if ($this->packages->has($name, $locale)) {
+ return $this->createInstance($name, $locale);
+ }
+
+ if (isset($this->_loaders[$name])) {
+ $package = $this->_loaders[$name]($name, $locale);
+ } else {
+ $package = $this->_loaders[static::FALLBACK_LOADER]($name, $locale);
+ }
+
+ $package = $this->setFallbackPackage($name, $package);
+ $this->packages->set($name, $locale, $package);
+
+ return $this->createInstance($name, $locale);
+ }
+
+ /**
+ * Create translator instance.
+ *
+ * @param string $name The translator package to retrieve.
+ * @param string $locale The locale to use; if empty, uses the default locale.
+ * @return \Cake\I18n\Translator A translator object.
+ */
+ protected function createInstance(string $name, string $locale): Translator
+ {
+ $package = $this->packages->get($name, $locale);
+ $fallback = $package->getFallback();
+ if ($fallback !== null) {
+ $fallback = $this->get($fallback, $locale);
+ }
+ $formatter = $this->formatters->get($package->getFormatter());
+
+ return new Translator($locale, $package, $formatter, $fallback);
+ }
+
+ /**
+ * Registers a loader function for a package name that will be used as a fallback
+ * in case no package with that name can be found.
+ *
+ * Loader callbacks will get as first argument the package name and the locale as
+ * the second argument.
+ *
+ * @param string $name The name of the translator package to register a loader for
+ * @param callable $loader A callable object that should return a Package
+ * @return void
+ */
+ public function registerLoader(string $name, callable $loader): void
+ {
+ $this->_loaders[$name] = $loader;
+ }
+
+ /**
+ * Sets the name of the default messages formatter to use for future
+ * translator instances.
+ *
+ * If called with no arguments, it will return the currently configured value.
+ *
+ * @param string|null $name The name of the formatter to use.
+ * @return string The name of the formatter.
+ */
+ public function defaultFormatter(?string $name = null): string
+ {
+ if ($name === null) {
+ return $this->_defaultFormatter;
+ }
+
+ return $this->_defaultFormatter = $name;
+ }
+
+ /**
+ * Set if the default domain fallback is used.
+ *
+ * @param bool $enable flag to enable or disable fallback
+ * @return void
+ */
+ public function useFallback(bool $enable = true): void
+ {
+ $this->_useFallback = $enable;
+ }
+
+ /**
+ * Set fallback domain for package.
+ *
+ * @param string $name The name of the package.
+ * @param \Cake\I18n\Package $package Package instance
+ * @return \Cake\I18n\Package
+ */
+ public function setFallbackPackage(string $name, Package $package): Package
+ {
+ if ($package->getFallback()) {
+ return $package;
+ }
+
+ $fallbackDomain = null;
+ if ($this->_useFallback && $name !== 'default') {
+ $fallbackDomain = 'default';
+ }
+
+ $package->setFallback($fallbackDomain);
+
+ return $package;
+ }
+
+ /**
+ * Set domain fallback for loader.
+ *
+ * @param string $name The name of the loader domain
+ * @param callable $loader invokable loader
+ * @return callable loader
+ */
+ public function setLoaderFallback(string $name, callable $loader): callable
+ {
+ $fallbackDomain = 'default';
+ if (!$this->_useFallback || $name === $fallbackDomain) {
+ return $loader;
+ }
+ $loader = function () use ($loader, $fallbackDomain) {
+ /** @var \Cake\I18n\Package $package */
+ $package = $loader();
+ if (!$package->getFallback()) {
+ $package->setFallback($fallbackDomain);
+ }
+
+ return $package;
+ };
+
+ return $loader;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/composer.json b/app/vendor/cakephp/cakephp/src/I18n/composer.json
new file mode 100644
index 000000000..8b75144a9
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/composer.json
@@ -0,0 +1,47 @@
+{
+ "name": "cakephp/i18n",
+ "description": "CakePHP Internationalization library with support for messages translation and dates and numbers localization",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "i18n",
+ "internationalisation",
+ "internationalization",
+ "localisation",
+ "localization",
+ "translation",
+ "date",
+ "number"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/i18n/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/i18n"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "ext-intl": "*",
+ "cakephp/core": "^4.0",
+ "cakephp/chronos": "^2.0.0"
+ },
+ "suggest": {
+ "cakephp/cache": "Require this if you want automatic caching of translators"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\I18n\\": "."
+ },
+ "files": [
+ "functions.php"
+ ]
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/I18n/functions.php b/app/vendor/cakephp/cakephp/src/I18n/functions.php
new file mode 100644
index 000000000..45f17250f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/I18n/functions.php
@@ -0,0 +1,253 @@
+translate($singular, $args);
+ }
+
+}
+
+if (!function_exists('__n')) {
+ /**
+ * Returns correct plural form of message identified by $singular and $plural for count $count.
+ * Some languages have more than one form for plural messages dependent on the count.
+ *
+ * @param string $singular Singular text to translate.
+ * @param string $plural Plural text.
+ * @param int $count Count.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Plural form of translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__n
+ */
+ function __n(string $singular, string $plural, int $count, ...$args): string
+ {
+ if (!$singular) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator()->translate(
+ $plural,
+ ['_count' => $count, '_singular' => $singular] + $args
+ );
+ }
+
+}
+
+if (!function_exists('__d')) {
+ /**
+ * Allows you to override the current domain for a single message lookup.
+ *
+ * @param string $domain Domain.
+ * @param string $msg String to translate.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__d
+ */
+ function __d(string $domain, string $msg, ...$args): string
+ {
+ if (!$msg) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator($domain)->translate($msg, $args);
+ }
+
+}
+
+if (!function_exists('__dn')) {
+ /**
+ * Allows you to override the current domain for a single plural message lookup.
+ * Returns correct plural form of message identified by $singular and $plural for count $count
+ * from domain $domain.
+ *
+ * @param string $domain Domain.
+ * @param string $singular Singular string to translate.
+ * @param string $plural Plural.
+ * @param int $count Count.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Plural form of translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__dn
+ */
+ function __dn(string $domain, string $singular, string $plural, int $count, ...$args): string
+ {
+ if (!$singular) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator($domain)->translate(
+ $plural,
+ ['_count' => $count, '_singular' => $singular] + $args
+ );
+ }
+
+}
+
+if (!function_exists('__x')) {
+ /**
+ * Returns a translated string if one is found; Otherwise, the submitted message.
+ * The context is a unique identifier for the translations string that makes it unique
+ * within the same domain.
+ *
+ * @param string $context Context of the text.
+ * @param string $singular Text to translate.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__x
+ */
+ function __x(string $context, string $singular, ...$args): string
+ {
+ if (!$singular) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator()->translate($singular, ['_context' => $context] + $args);
+ }
+
+}
+
+if (!function_exists('__xn')) {
+ /**
+ * Returns correct plural form of message identified by $singular and $plural for count $count.
+ * Some languages have more than one form for plural messages dependent on the count.
+ * The context is a unique identifier for the translations string that makes it unique
+ * within the same domain.
+ *
+ * @param string $context Context of the text.
+ * @param string $singular Singular text to translate.
+ * @param string $plural Plural text.
+ * @param int $count Count.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Plural form of translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__xn
+ */
+ function __xn(string $context, string $singular, string $plural, int $count, ...$args): string
+ {
+ if (!$singular) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator()->translate(
+ $plural,
+ ['_count' => $count, '_singular' => $singular, '_context' => $context] + $args
+ );
+ }
+
+}
+
+if (!function_exists('__dx')) {
+ /**
+ * Allows you to override the current domain for a single message lookup.
+ * The context is a unique identifier for the translations string that makes it unique
+ * within the same domain.
+ *
+ * @param string $domain Domain.
+ * @param string $context Context of the text.
+ * @param string $msg String to translate.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__dx
+ */
+ function __dx(string $domain, string $context, string $msg, ...$args): string
+ {
+ if (!$msg) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator($domain)->translate(
+ $msg,
+ ['_context' => $context] + $args
+ );
+ }
+
+}
+
+if (!function_exists('__dxn')) {
+ /**
+ * Returns correct plural form of message identified by $singular and $plural for count $count.
+ * Allows you to override the current domain for a single message lookup.
+ * The context is a unique identifier for the translations string that makes it unique
+ * within the same domain.
+ *
+ * @param string $domain Domain.
+ * @param string $context Context of the text.
+ * @param string $singular Singular text to translate.
+ * @param string $plural Plural text.
+ * @param int $count Count.
+ * @param mixed ...$args Array with arguments or multiple arguments in function.
+ * @return string Plural form of translated string.
+ * @link https://book.cakephp.org/4/en/core-libraries/global-constants-and-functions.html#__dxn
+ */
+ function __dxn(string $domain, string $context, string $singular, string $plural, int $count, ...$args): string
+ {
+ if (!$singular) {
+ return '';
+ }
+ if (isset($args[0]) && is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ return I18n::getTranslator($domain)->translate(
+ $plural,
+ ['_count' => $count, '_singular' => $singular, '_context' => $context] + $args
+ );
+ }
+
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/ArrayLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/ArrayLog.php
new file mode 100644
index 000000000..939b90119
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/Engine/ArrayLog.php
@@ -0,0 +1,68 @@
+content[] = $level . ' ' . $this->_format($message, $context);
+ }
+
+ /**
+ * Read the internal storage
+ *
+ * @return string[]
+ */
+ public function read(): array
+ {
+ return $this->content;
+ }
+
+ /**
+ * Reset internal storage.
+ *
+ * @return void
+ */
+ public function clear(): void
+ {
+ $this->content = [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/BaseLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/BaseLog.php
new file mode 100644
index 000000000..03edaa0ed
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/Engine/BaseLog.php
@@ -0,0 +1,177 @@
+ [],
+ 'scopes' => [],
+ 'dateFormat' => 'Y-m-d H:i:s',
+ ];
+
+ /**
+ * __construct method
+ *
+ * @param array $config Configuration array
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+
+ if (!is_array($this->_config['scopes']) && $this->_config['scopes'] !== false) {
+ $this->_config['scopes'] = (array)$this->_config['scopes'];
+ }
+
+ if (!is_array($this->_config['levels'])) {
+ $this->_config['levels'] = (array)$this->_config['levels'];
+ }
+
+ if (!empty($this->_config['types']) && empty($this->_config['levels'])) {
+ $this->_config['levels'] = (array)$this->_config['types'];
+ }
+ }
+
+ /**
+ * Get the levels this logger is interested in.
+ *
+ * @return array
+ */
+ public function levels(): array
+ {
+ return $this->_config['levels'];
+ }
+
+ /**
+ * Get the scopes this logger is interested in.
+ *
+ * @return array|false
+ */
+ public function scopes()
+ {
+ return $this->_config['scopes'];
+ }
+
+ /**
+ * Formats the message to be logged.
+ *
+ * The context can optionally be used by log engines to interpolate variables
+ * or add additional info to the logged message.
+ *
+ * @param string $message The message to be formatted.
+ * @param array $context Additional logging information for the message.
+ * @return string
+ */
+ protected function _format(string $message, array $context = []): string
+ {
+ if (strpos($message, '{') === false && strpos($message, '}') === false) {
+ return $message;
+ }
+
+ preg_match_all(
+ '/(?getArrayCopy(), JSON_UNESCAPED_UNICODE);
+ continue;
+ }
+
+ if ($value instanceof Serializable) {
+ $replacements['{' . $key . '}'] = $value->serialize();
+ continue;
+ }
+
+ if (is_object($value)) {
+ if (method_exists($value, '__toString')) {
+ $replacements['{' . $key . '}'] = (string)$value;
+ continue;
+ }
+
+ if (method_exists($value, 'toArray')) {
+ $replacements['{' . $key . '}'] = json_encode($value->toArray(), JSON_UNESCAPED_UNICODE);
+ continue;
+ }
+
+ if (method_exists($value, '__debugInfo')) {
+ $replacements['{' . $key . '}'] = json_encode($value->__debugInfo(), JSON_UNESCAPED_UNICODE);
+ continue;
+ }
+ }
+
+ $replacements['{' . $key . '}'] = sprintf('[unhandled value of type %s]', getTypeName($value));
+ }
+
+ /** @psalm-suppress InvalidArgument */
+ return str_replace(array_keys($replacements), $replacements, $message);
+ }
+
+ /**
+ * Returns date formatted according to given `dateFormat` option format.
+ *
+ * This function affects `FileLog` or` ConsoleLog` datetime information format.
+ *
+ * @return string
+ */
+ protected function _getFormattedDate(): string
+ {
+ return (new DateTimeImmutable())->format($this->_config['dateFormat']);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/ConsoleLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/ConsoleLog.php
new file mode 100644
index 000000000..76e02ad88
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/Engine/ConsoleLog.php
@@ -0,0 +1,95 @@
+ 'php://stderr',
+ 'levels' => null,
+ 'scopes' => [],
+ 'outputAs' => null,
+ 'dateFormat' => 'Y-m-d H:i:s',
+ ];
+
+ /**
+ * Output stream
+ *
+ * @var \Cake\Console\ConsoleOutput
+ */
+ protected $_output;
+
+ /**
+ * Constructs a new Console Logger.
+ *
+ * Config
+ *
+ * - `levels` string or array, levels the engine is interested in
+ * - `scopes` string or array, scopes the engine is interested in
+ * - `stream` the path to save logs on.
+ * - `outputAs` integer or ConsoleOutput::[RAW|PLAIN|COLOR]
+ * - `dateFormat` PHP date() format.
+ *
+ * @param array $config Options for the FileLog, see above.
+ * @throws \InvalidArgumentException
+ */
+ public function __construct(array $config = [])
+ {
+ parent::__construct($config);
+
+ $config = $this->_config;
+ if ($config['stream'] instanceof ConsoleOutput) {
+ $this->_output = $config['stream'];
+ } elseif (is_string($config['stream'])) {
+ $this->_output = new ConsoleOutput($config['stream']);
+ } else {
+ throw new InvalidArgumentException('`stream` not a ConsoleOutput nor string');
+ }
+
+ if (isset($config['outputAs'])) {
+ $this->_output->setOutputAs($config['outputAs']);
+ }
+ }
+
+ /**
+ * Implements writing to console.
+ *
+ * @param mixed $level The severity level of log you are making.
+ * @param string $message The message you want to log.
+ * @param array $context Additional information about the logged message
+ * @return void success of write.
+ * @see Cake\Log\Log::$_levels
+ */
+ public function log($level, $message, array $context = [])
+ {
+ $message = $this->_format($message, $context);
+ $output = $this->_getFormattedDate() . ' ' . ucfirst($level) . ': ' . $message;
+
+ $this->_output->write(sprintf('<%s>%s%s>', $level, $output, $level));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php
new file mode 100644
index 000000000..b844a8c8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/Engine/FileLog.php
@@ -0,0 +1,211 @@
+ null,
+ 'file' => null,
+ 'types' => null,
+ 'levels' => [],
+ 'scopes' => [],
+ 'rotate' => 10,
+ 'size' => 10485760, // 10MB
+ 'mask' => null,
+ 'dateFormat' => 'Y-m-d H:i:s',
+ ];
+
+ /**
+ * Path to save log files on.
+ *
+ * @var string
+ */
+ protected $_path;
+
+ /**
+ * The name of the file to save logs into.
+ *
+ * @var string|null
+ */
+ protected $_file;
+
+ /**
+ * Max file size, used for log file rotation.
+ *
+ * @var int|null
+ */
+ protected $_size;
+
+ /**
+ * Sets protected properties based on config provided
+ *
+ * @param array $config Configuration array
+ */
+ public function __construct(array $config = [])
+ {
+ parent::__construct($config);
+
+ $this->_path = $this->getConfig('path', sys_get_temp_dir() . DIRECTORY_SEPARATOR);
+ if (Configure::read('debug') && !is_dir($this->_path)) {
+ mkdir($this->_path, 0775, true);
+ }
+
+ if (!empty($this->_config['file'])) {
+ $this->_file = $this->_config['file'];
+ if (substr($this->_file, -4) !== '.log') {
+ $this->_file .= '.log';
+ }
+ }
+
+ if (!empty($this->_config['size'])) {
+ if (is_numeric($this->_config['size'])) {
+ $this->_size = (int)$this->_config['size'];
+ } else {
+ $this->_size = Text::parseFileSize($this->_config['size']);
+ }
+ }
+ }
+
+ /**
+ * Implements writing to log files.
+ *
+ * @param mixed $level The severity level of the message being written.
+ * @param string $message The message you want to log.
+ * @param array $context Additional information about the logged message
+ * @return void
+ * @see Cake\Log\Log::$_levels
+ */
+ public function log($level, $message, array $context = []): void
+ {
+ $message = $this->_format($message, $context);
+ $output = $this->_getFormattedDate() . ' ' . ucfirst($level) . ': ' . $message . "\n";
+ $filename = $this->_getFilename($level);
+ if ($this->_size) {
+ $this->_rotateFile($filename);
+ }
+
+ $pathname = $this->_path . $filename;
+ $mask = $this->_config['mask'];
+ if (!$mask) {
+ file_put_contents($pathname, $output, FILE_APPEND);
+
+ return;
+ }
+
+ $exists = is_file($pathname);
+ file_put_contents($pathname, $output, FILE_APPEND);
+ static $selfError = false;
+
+ if (!$selfError && !$exists && !chmod($pathname, (int)$mask)) {
+ $selfError = true;
+ trigger_error(vsprintf(
+ 'Could not apply permission mask "%s" on log file "%s"',
+ [$mask, $pathname]
+ ), E_USER_WARNING);
+ $selfError = false;
+ }
+ }
+
+ /**
+ * Get filename
+ *
+ * @param string $level The level of log.
+ * @return string File name
+ */
+ protected function _getFilename(string $level): string
+ {
+ $debugTypes = ['notice', 'info', 'debug'];
+
+ if ($this->_file) {
+ $filename = $this->_file;
+ } elseif ($level === 'error' || $level === 'warning') {
+ $filename = 'error.log';
+ } elseif (in_array($level, $debugTypes, true)) {
+ $filename = 'debug.log';
+ } else {
+ $filename = $level . '.log';
+ }
+
+ return $filename;
+ }
+
+ /**
+ * Rotate log file if size specified in config is reached.
+ * Also if `rotate` count is reached oldest file is removed.
+ *
+ * @param string $filename Log file name
+ * @return bool|null True if rotated successfully or false in case of error.
+ * Null if file doesn't need to be rotated.
+ */
+ protected function _rotateFile(string $filename): ?bool
+ {
+ $filePath = $this->_path . $filename;
+ clearstatcache(true, $filePath);
+
+ if (
+ !is_file($filePath) ||
+ filesize($filePath) < $this->_size
+ ) {
+ return null;
+ }
+
+ $rotate = $this->_config['rotate'];
+ if ($rotate === 0) {
+ $result = unlink($filePath);
+ } else {
+ $result = rename($filePath, $filePath . '.' . time());
+ }
+
+ $files = glob($filePath . '.*');
+ if ($files) {
+ $filesToDelete = count($files) - $rotate;
+ while ($filesToDelete > 0) {
+ unlink(array_shift($files));
+ $filesToDelete--;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/Engine/SyslogLog.php b/app/vendor/cakephp/cakephp/src/Log/Engine/SyslogLog.php
new file mode 100644
index 000000000..2fc469e20
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/Engine/SyslogLog.php
@@ -0,0 +1,150 @@
+ 'Syslog',
+ * 'levels' => ['emergency', 'alert', 'critical', 'error'],
+ * 'format' => "%s: My-App - %s",
+ * 'prefix' => 'Web Server 01'
+ * ]);
+ * ```
+ *
+ * @var array
+ */
+ protected $_defaultConfig = [
+ 'levels' => [],
+ 'scopes' => [],
+ 'format' => '%s: %s',
+ 'flag' => LOG_ODELAY,
+ 'prefix' => '',
+ 'facility' => LOG_USER,
+ ];
+
+ /**
+ * Used to map the string names back to their LOG_* constants
+ *
+ * @var int[]
+ */
+ protected $_levelMap = [
+ 'emergency' => LOG_EMERG,
+ 'alert' => LOG_ALERT,
+ 'critical' => LOG_CRIT,
+ 'error' => LOG_ERR,
+ 'warning' => LOG_WARNING,
+ 'notice' => LOG_NOTICE,
+ 'info' => LOG_INFO,
+ 'debug' => LOG_DEBUG,
+ ];
+
+ /**
+ * Whether the logger connection is open or not
+ *
+ * @var bool
+ */
+ protected $_open = false;
+
+ /**
+ * Writes a message to syslog
+ *
+ * Map the $level back to a LOG_ constant value, split multi-line messages into multiple
+ * log messages, pass all messages through the format defined in the configuration
+ *
+ * @param mixed $level The severity level of log you are making.
+ * @param string $message The message you want to log.
+ * @param array $context Additional information about the logged message
+ * @return void
+ * @see Cake\Log\Log::$_levels
+ */
+ public function log($level, $message, array $context = []): void
+ {
+ if (!$this->_open) {
+ $config = $this->_config;
+ $this->_open($config['prefix'], $config['flag'], $config['facility']);
+ $this->_open = true;
+ }
+
+ $priority = LOG_DEBUG;
+ if (isset($this->_levelMap[$level])) {
+ $priority = $this->_levelMap[$level];
+ }
+
+ $messages = explode("\n", $this->_format($message, $context));
+ foreach ($messages as $message) {
+ $message = sprintf($this->_config['format'], $level, $message);
+ $this->_write($priority, $message);
+ }
+ }
+
+ /**
+ * Extracts the call to openlog() in order to run unit tests on it. This function
+ * will initialize the connection to the system logger
+ *
+ * @param string $ident the prefix to add to all messages logged
+ * @param int $options the options flags to be used for logged messages
+ * @param int $facility the stream or facility to log to
+ * @return void
+ */
+ protected function _open(string $ident, int $options, int $facility): void
+ {
+ openlog($ident, $options, $facility);
+ }
+
+ /**
+ * Extracts the call to syslog() in order to run unit tests on it. This function
+ * will perform the actual write in the system logger
+ *
+ * @param int $priority Message priority.
+ * @param string $message Message to log.
+ * @return bool
+ */
+ protected function _write(int $priority, string $message): bool
+ {
+ return syslog($priority, $message);
+ }
+
+ /**
+ * Closes the logger connection
+ */
+ public function __destruct()
+ {
+ closelog();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Log/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Log/Log.php b/app/vendor/cakephp/cakephp/src/Log/Log.php
new file mode 100644
index 000000000..1d102c305
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/Log.php
@@ -0,0 +1,525 @@
+ 'FileLog']);
+ * ```
+ *
+ * You can define the className as any fully namespaced classname or use a short hand
+ * classname to use loggers in the `App\Log\Engine` & `Cake\Log\Engine` namespaces.
+ * You can also use plugin short hand to use logging classes provided by plugins.
+ *
+ * Log adapters are required to implement `Psr\Log\LoggerInterface`, and there is a
+ * built-in base class (`Cake\Log\Engine\BaseLog`) that can be used for custom loggers.
+ *
+ * Outside of the `className` key, all other configuration values will be passed to the
+ * logging adapter's constructor as an array.
+ *
+ * ### Logging levels
+ *
+ * When configuring loggers, you can set which levels a logger will handle.
+ * This allows you to disable debug messages in production for example:
+ *
+ * ```
+ * Log::setConfig('default', [
+ * 'className' => 'File',
+ * 'path' => LOGS,
+ * 'levels' => ['error', 'critical', 'alert', 'emergency']
+ * ]);
+ * ```
+ *
+ * The above logger would only log error messages or higher. Any
+ * other log messages would be discarded.
+ *
+ * ### Logging scopes
+ *
+ * When configuring loggers you can define the active scopes the logger
+ * is for. If defined, only the listed scopes will be handled by the
+ * logger. If you don't define any scopes an adapter will catch
+ * all scopes that match the handled levels.
+ *
+ * ```
+ * Log::setConfig('payments', [
+ * 'className' => 'File',
+ * 'scopes' => ['payment', 'order']
+ * ]);
+ * ```
+ *
+ * The above logger will only capture log entries made in the
+ * `payment` and `order` scopes. All other scopes including the
+ * undefined scope will be ignored.
+ *
+ * ### Writing to the log
+ *
+ * You write to the logs using Log::write(). See its documentation for more information.
+ *
+ * ### Logging Levels
+ *
+ * By default Cake Log supports all the log levels defined in
+ * RFC 5424. When logging messages you can either use the named methods,
+ * or the correct constants with `write()`:
+ *
+ * ```
+ * Log::error('Something horrible happened');
+ * Log::write(LOG_ERR, 'Something horrible happened');
+ * ```
+ *
+ * ### Logging scopes
+ *
+ * When logging messages and configuring log adapters, you can specify
+ * 'scopes' that the logger will handle. You can think of scopes as subsystems
+ * in your application that may require different logging setups. For
+ * example in an e-commerce application you may want to handle logged errors
+ * in the cart and ordering subsystems differently than the rest of the
+ * application. By using scopes you can control logging for each part
+ * of your application and also use standard log levels.
+ */
+class Log
+{
+ use StaticConfigTrait {
+ setConfig as protected _setConfig;
+ }
+
+ /**
+ * An array mapping url schemes to fully qualified Log engine class names
+ *
+ * @var string[]
+ * @psalm-var array
+ */
+ protected static $_dsnClassMap = [
+ 'console' => Engine\ConsoleLog::class,
+ 'file' => Engine\FileLog::class,
+ 'syslog' => Engine\SyslogLog::class,
+ ];
+
+ /**
+ * Internal flag for tracking whether or not configuration has been changed.
+ *
+ * @var bool
+ */
+ protected static $_dirtyConfig = false;
+
+ /**
+ * LogEngineRegistry class
+ *
+ * @var \Cake\Log\LogEngineRegistry
+ */
+ protected static $_registry;
+
+ /**
+ * Handled log levels
+ *
+ * @var string[]
+ */
+ protected static $_levels = [
+ 'emergency',
+ 'alert',
+ 'critical',
+ 'error',
+ 'warning',
+ 'notice',
+ 'info',
+ 'debug',
+ ];
+
+ /**
+ * Log levels as detailed in RFC 5424
+ * https://tools.ietf.org/html/rfc5424
+ *
+ * @var array
+ */
+ protected static $_levelMap = [
+ 'emergency' => LOG_EMERG,
+ 'alert' => LOG_ALERT,
+ 'critical' => LOG_CRIT,
+ 'error' => LOG_ERR,
+ 'warning' => LOG_WARNING,
+ 'notice' => LOG_NOTICE,
+ 'info' => LOG_INFO,
+ 'debug' => LOG_DEBUG,
+ ];
+
+ /**
+ * Initializes registry and configurations
+ *
+ * @return void
+ */
+ protected static function _init(): void
+ {
+ if (empty(static::$_registry)) {
+ static::$_registry = new LogEngineRegistry();
+ }
+ if (static::$_dirtyConfig) {
+ static::_loadConfig();
+ }
+ static::$_dirtyConfig = false;
+ }
+
+ /**
+ * Load the defined configuration and create all the defined logging
+ * adapters.
+ *
+ * @return void
+ */
+ protected static function _loadConfig(): void
+ {
+ foreach (static::$_config as $name => $properties) {
+ if (isset($properties['engine'])) {
+ $properties['className'] = $properties['engine'];
+ }
+ if (!static::$_registry->has((string)$name)) {
+ static::$_registry->load((string)$name, $properties);
+ }
+ }
+ }
+
+ /**
+ * Reset all the connected loggers. This is useful to do when changing the logging
+ * configuration or during testing when you want to reset the internal state of the
+ * Log class.
+ *
+ * Resets the configured logging adapters, as well as any custom logging levels.
+ * This will also clear the configuration data.
+ *
+ * @return void
+ */
+ public static function reset(): void
+ {
+ if (!empty(static::$_registry)) {
+ static::$_registry->reset();
+ }
+ static::$_config = [];
+ static::$_dirtyConfig = true;
+ }
+
+ /**
+ * Gets log levels
+ *
+ * Call this method to obtain current
+ * level configuration.
+ *
+ * @return string[] Active log levels
+ */
+ public static function levels(): array
+ {
+ return static::$_levels;
+ }
+
+ /**
+ * This method can be used to define logging adapters for an application
+ * or read existing configuration.
+ *
+ * To change an adapter's configuration at runtime, first drop the adapter and then
+ * reconfigure it.
+ *
+ * Loggers will not be constructed until the first log message is written.
+ *
+ * ### Usage
+ *
+ * Setting a cache engine up.
+ *
+ * ```
+ * Log::setConfig('default', $settings);
+ * ```
+ *
+ * Injecting a constructed adapter in:
+ *
+ * ```
+ * Log::setConfig('default', $instance);
+ * ```
+ *
+ * Using a factory function to get an adapter:
+ *
+ * ```
+ * Log::setConfig('default', function () { return new FileLog(); });
+ * ```
+ *
+ * Configure multiple adapters at once:
+ *
+ * ```
+ * Log::setConfig($arrayOfConfig);
+ * ```
+ *
+ * @param string|array $key The name of the logger config, or an array of multiple configs.
+ * @param array|null $config An array of name => config data for adapter.
+ * @return void
+ * @throws \BadMethodCallException When trying to modify an existing config.
+ */
+ public static function setConfig($key, $config = null): void
+ {
+ static::_setConfig($key, $config);
+ static::$_dirtyConfig = true;
+ }
+
+ /**
+ * Get a logging engine.
+ *
+ * @param string $name Key name of a configured adapter to get.
+ * @return \Psr\Log\LoggerInterface|null Instance of LoggerInterface or false if not found
+ */
+ public static function engine(string $name): ?LoggerInterface
+ {
+ static::_init();
+ if (static::$_registry->{$name}) {
+ return static::$_registry->{$name};
+ }
+
+ return null;
+ }
+
+ /**
+ * Writes the given message and type to all of the configured log adapters.
+ * Configured adapters are passed both the $level and $message variables. $level
+ * is one of the following strings/values.
+ *
+ * ### Levels:
+ *
+ * - `LOG_EMERG` => 'emergency',
+ * - `LOG_ALERT` => 'alert',
+ * - `LOG_CRIT` => 'critical',
+ * - `LOG_ERR` => 'error',
+ * - `LOG_WARNING` => 'warning',
+ * - `LOG_NOTICE` => 'notice',
+ * - `LOG_INFO` => 'info',
+ * - `LOG_DEBUG` => 'debug',
+ *
+ * ### Basic usage
+ *
+ * Write a 'warning' message to the logs:
+ *
+ * ```
+ * Log::write('warning', 'Stuff is broken here');
+ * ```
+ *
+ * ### Using scopes
+ *
+ * When writing a log message you can define one or many scopes for the message.
+ * This allows you to handle messages differently based on application section/feature.
+ *
+ * ```
+ * Log::write('warning', 'Payment failed', ['scope' => 'payment']);
+ * ```
+ *
+ * When configuring loggers you can configure the scopes a particular logger will handle.
+ * When using scopes, you must ensure that the level of the message, and the scope of the message
+ * intersect with the defined levels & scopes for a logger.
+ *
+ * ### Unhandled log messages
+ *
+ * If no configured logger can handle a log message (because of level or scope restrictions)
+ * then the logged message will be ignored and silently dropped. You can check if this has happened
+ * by inspecting the return of write(). If false the message was not handled.
+ *
+ * @param int|string $level The severity level of the message being written.
+ * The value must be an integer or string matching a known level.
+ * @param string $message Message content to log
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ * @throws \InvalidArgumentException If invalid level is passed.
+ */
+ public static function write($level, string $message, $context = []): bool
+ {
+ static::_init();
+ if (is_int($level) && in_array($level, static::$_levelMap, true)) {
+ $level = array_search($level, static::$_levelMap, true);
+ }
+
+ if (!in_array($level, static::$_levels, true)) {
+ /** @psalm-suppress PossiblyFalseArgument */
+ throw new InvalidArgumentException(sprintf('Invalid log level `%s`', $level));
+ }
+
+ $logged = false;
+ $context = (array)$context;
+ if (isset($context[0])) {
+ $context = ['scope' => $context];
+ }
+ $context += ['scope' => []];
+
+ foreach (static::$_registry->loaded() as $streamName) {
+ $logger = static::$_registry->{$streamName};
+ $levels = $scopes = null;
+
+ if ($logger instanceof BaseLog) {
+ $levels = $logger->levels();
+ $scopes = $logger->scopes();
+ }
+ if ($scopes === null) {
+ $scopes = [];
+ }
+
+ $correctLevel = empty($levels) || in_array($level, $levels, true);
+ $inScope = $scopes === false && empty($context['scope']) || $scopes === [] ||
+ is_array($scopes) && array_intersect((array)$context['scope'], $scopes);
+
+ if ($correctLevel && $inScope) {
+ $logger->log($level, $message, $context);
+ $logged = true;
+ }
+ }
+
+ return $logged;
+ }
+
+ /**
+ * Convenience method to log emergency messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function emergency(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log alert messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function alert(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log critical messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function critical(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log error messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function error(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log warning messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function warning(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log notice messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function notice(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log debug messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function debug(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+
+ /**
+ * Convenience method to log info messages
+ *
+ * @param string $message log message
+ * @param string|array $context Additional data to be used for logging the message.
+ * The special `scope` key can be passed to be used for further filtering of the
+ * log engines to be used. If a string or a numerically index array is passed, it
+ * will be treated as the `scope` key.
+ * See Cake\Log\Log::setConfig() for more information on logging scopes.
+ * @return bool Success
+ */
+ public static function info(string $message, $context = []): bool
+ {
+ return static::write(__FUNCTION__, $message, $context);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/LogEngineRegistry.php b/app/vendor/cakephp/cakephp/src/Log/LogEngineRegistry.php
new file mode 100644
index 000000000..92fd1ee2e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/LogEngineRegistry.php
@@ -0,0 +1,109 @@
+
+ */
+class LogEngineRegistry extends ObjectRegistry
+{
+ /**
+ * Resolve a logger classname.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class Partial classname to resolve.
+ * @return string|null Either the correct class name or null.
+ * @psalm-return class-string|null
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ return App::className($class, 'Log/Engine', 'Log');
+ }
+
+ /**
+ * Throws an exception when a logger is missing.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class The classname that is missing.
+ * @param string|null $plugin The plugin the logger is missing in.
+ * @return void
+ * @throws \RuntimeException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ throw new RuntimeException(sprintf('Could not load class %s', $class));
+ }
+
+ /**
+ * Create the logger instance.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string|\Psr\Log\LoggerInterface $class The classname or object to make.
+ * @param string $alias The alias of the object.
+ * @param array $config An array of settings to use for the logger.
+ * @return \Psr\Log\LoggerInterface The constructed logger class.
+ * @throws \RuntimeException when an object doesn't implement the correct interface.
+ */
+ protected function _create($class, string $alias, array $config): LoggerInterface
+ {
+ if (is_callable($class)) {
+ $class = $class($alias);
+ }
+
+ if (is_object($class)) {
+ $instance = $class;
+ }
+
+ if (!isset($instance)) {
+ /** @psalm-suppress UndefinedClass */
+ $instance = new $class($config);
+ }
+
+ if ($instance instanceof LoggerInterface) {
+ return $instance;
+ }
+
+ throw new RuntimeException(sprintf(
+ 'Loggers must implement %s. Found `%s` instance instead.',
+ LoggerInterface::class,
+ getTypeName($instance)
+ ));
+ }
+
+ /**
+ * Remove a single logger from the registry.
+ *
+ * @param string $name The logger name.
+ * @return $this
+ */
+ public function unload(string $name)
+ {
+ unset($this->_loaded[$name]);
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Log/LogTrait.php b/app/vendor/cakephp/cakephp/src/Log/LogTrait.php
new file mode 100644
index 000000000..e87b061d8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/LogTrait.php
@@ -0,0 +1,39 @@
+ 'FileLog',
+ 'levels' => ['notice', 'info', 'debug'],
+ 'file' => '/path/to/file.log',
+]);
+
+// Fully namespaced name.
+Log::config('production', [
+ 'className' => \Cake\Log\Engine\SyslogLog::class,
+ 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
+]);
+```
+
+It is also possible to create loggers by providing a closure.
+
+```php
+Log::config('special', function () {
+ // Return any PSR-3 compatible logger
+ return new MyPSR3CompatibleLogger();
+});
+```
+
+Or by injecting an instance directly:
+
+```php
+Log::config('special', new MyPSR3CompatibleLogger());
+```
+
+You can then use the `Log` class to pass messages to the logging backends:
+
+```php
+Log::write('debug', 'Something did not work');
+```
+
+Only the logging engines subscribed to the log level you are writing to will
+get the message passed. In the example above, only the 'local' engine will get
+the log message.
+
+### Filtering messages with scopes
+
+The Log library supports another level of message filtering. By using scopes,
+you can limit the logging engines that receive a particular message.
+
+```php
+// Configure /logs/payments.log to receive all levels, but only
+// those with `payments` scope.
+Log::config('payments', [
+ 'className' => 'FileLog',
+ 'levels' => ['error', 'info', 'warning'],
+ 'scopes' => ['payments'],
+ 'file' => '/logs/payments.log',
+]);
+
+Log::warning('this gets written only to payments.log', ['scope' => ['payments']]);
+```
+
+## Documentation
+
+Please make sure you check the [official documentation](https://book.cakephp.org/4/en/core-libraries/logging.html)
diff --git a/app/vendor/cakephp/cakephp/src/Log/composer.json b/app/vendor/cakephp/cakephp/src/Log/composer.json
new file mode 100644
index 000000000..0d43640be
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Log/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "cakephp/log",
+ "description": "CakePHP logging library with support for multiple different streams",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "log",
+ "logging",
+ "streams"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/log/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/log"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/core": "^4.0",
+ "psr/log": "^1.0.0"
+ },
+ "provide": {
+ "psr/log-implementation": "^1.0.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Log\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/AbstractTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/AbstractTransport.php
new file mode 100644
index 000000000..89786f4d2
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/AbstractTransport.php
@@ -0,0 +1,75 @@
+setConfig($config);
+ }
+
+ /**
+ * Check that at least one destination header is set.
+ *
+ * @param \Cake\Mailer\Message $message Message instance.
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException If at least one of to, cc or bcc is not specified.
+ */
+ protected function checkRecipient(Message $message): void
+ {
+ if (
+ $message->getTo() === []
+ && $message->getCc() === []
+ && $message->getBcc() === []
+ ) {
+ throw new CakeException(
+ 'You must specify at least one recipient.'
+ . ' Use one of `setTo`, `setCc` or `setBcc` to define a recipient.'
+ );
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Email.php b/app/vendor/cakephp/cakephp/src/Mailer/Email.php
new file mode 100644
index 000000000..0362070e0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/Email.php
@@ -0,0 +1,621 @@
+
+ */
+ protected $messageClass = Message::class;
+
+ /**
+ * Message instance.
+ *
+ * @var \Cake\Mailer\Message
+ */
+ protected $message;
+
+ /**
+ * Constructor
+ *
+ * @param array|string|null $config Array of configs, or string to load configs from app.php
+ */
+ public function __construct($config = null)
+ {
+ $this->message = new $this->messageClass();
+
+ if ($config === null) {
+ $config = Mailer::getConfig('default');
+ }
+
+ if ($config) {
+ $this->setProfile($config);
+ }
+ }
+
+ /**
+ * Clone Renderer instance when email object is cloned.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ if ($this->renderer) {
+ $this->renderer = clone $this->renderer;
+ }
+
+ if ($this->message !== null) {
+ $this->message = clone $this->message;
+ }
+ }
+
+ /**
+ * Magic method to forward method class to Email instance.
+ *
+ * @param string $method Method name.
+ * @param array $args Method arguments
+ * @return $this|mixed
+ */
+ public function __call(string $method, array $args)
+ {
+ $result = $this->message->$method(...$args);
+
+ if (strpos($method, 'get') === 0) {
+ return $result;
+ }
+
+ $getters = ['message'];
+ if (in_array($method, $getters, true)) {
+ return $result;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get message instance.
+ *
+ * @return \Cake\Mailer\Message
+ */
+ public function getMessage(): Message
+ {
+ return $this->message;
+ }
+
+ /**
+ * Sets view class for render.
+ *
+ * @param string $viewClass View class name.
+ * @return $this
+ */
+ public function setViewRenderer(string $viewClass)
+ {
+ $this->getRenderer()->viewBuilder()->setClassName($viewClass);
+
+ return $this;
+ }
+
+ /**
+ * Gets view class for render.
+ *
+ * @return string
+ * @psalm-suppress InvalidNullableReturnType
+ */
+ public function getViewRenderer(): string
+ {
+ /** @psalm-suppress NullableReturnStatement */
+ return $this->getRenderer()->viewBuilder()->getClassName();
+ }
+
+ /**
+ * Sets variables to be set on render.
+ *
+ * @param array $viewVars Variables to set for view.
+ * @return $this
+ */
+ public function setViewVars(array $viewVars)
+ {
+ $this->getRenderer()->viewBuilder()->setVars($viewVars);
+
+ return $this;
+ }
+
+ /**
+ * Gets variables to be set on render.
+ *
+ * @return array
+ */
+ public function getViewVars(): array
+ {
+ return $this->getRenderer()->viewBuilder()->getVars();
+ }
+
+ /**
+ * Sets the transport.
+ *
+ * When setting the transport you can either use the name
+ * of a configured transport or supply a constructed transport.
+ *
+ * @param string|\Cake\Mailer\AbstractTransport $name Either the name of a configured
+ * transport, or a transport instance.
+ * @return $this
+ * @throws \LogicException When the chosen transport lacks a send method.
+ * @throws \InvalidArgumentException When $name is neither a string nor an object.
+ */
+ public function setTransport($name)
+ {
+ if (is_string($name)) {
+ $transport = TransportFactory::get($name);
+ } elseif (is_object($name)) {
+ $transport = $name;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'The value passed for the "$name" argument must be either a string, or an object, %s given.',
+ gettype($name)
+ ));
+ }
+ if (!method_exists($transport, 'send')) {
+ throw new LogicException(sprintf('The "%s" do not have send method.', get_class($transport)));
+ }
+
+ $this->_transport = $transport;
+
+ return $this;
+ }
+
+ /**
+ * Gets the transport.
+ *
+ * @return \Cake\Mailer\AbstractTransport|null
+ */
+ public function getTransport(): ?AbstractTransport
+ {
+ return $this->_transport;
+ }
+
+ /**
+ * Get generated message (used by transport classes)
+ *
+ * @param string|null $type Use MESSAGE_* constants or null to return the full message as array
+ * @return string|array String if type is given, array if type is null
+ */
+ public function message(?string $type = null)
+ {
+ if ($type === null) {
+ return $this->message->getBody();
+ }
+
+ $method = 'getBody' . ucfirst($type);
+
+ return $this->message->$method();
+ }
+
+ /**
+ * Sets the configuration profile to use for this instance.
+ *
+ * @param string|array $config String with configuration name, or
+ * an array with config.
+ * @return $this
+ */
+ public function setProfile($config)
+ {
+ if (is_string($config)) {
+ $name = $config;
+ $config = Mailer::getConfig($name);
+ if (empty($config)) {
+ throw new InvalidArgumentException(sprintf('Unknown email configuration "%s".', $name));
+ }
+ unset($name);
+ }
+
+ $this->_profile = array_merge($this->_profile, $config);
+
+ $simpleMethods = [
+ 'transport',
+ ];
+ foreach ($simpleMethods as $method) {
+ if (isset($config[$method])) {
+ $this->{'set' . ucfirst($method)}($config[$method]);
+ unset($config[$method]);
+ }
+ }
+
+ $viewBuilderMethods = [
+ 'template', 'layout', 'theme',
+ ];
+ foreach ($viewBuilderMethods as $method) {
+ if (array_key_exists($method, $config)) {
+ $this->getRenderer()->viewBuilder()->{'set' . ucfirst($method)}($config[$method]);
+ unset($config[$method]);
+ }
+ }
+
+ if (array_key_exists('helpers', $config)) {
+ $this->getRenderer()->viewBuilder()->setHelpers($config['helpers'], false);
+ unset($config['helpers']);
+ }
+ if (array_key_exists('viewRenderer', $config)) {
+ $this->getRenderer()->viewBuilder()->setClassName($config['viewRenderer']);
+ unset($config['viewRenderer']);
+ }
+ if (array_key_exists('viewVars', $config)) {
+ $this->getRenderer()->viewBuilder()->setVars($config['viewVars']);
+ unset($config['viewVars']);
+ }
+
+ $this->message->setConfig($config);
+
+ return $this;
+ }
+
+ /**
+ * Gets the configuration profile to use for this instance.
+ *
+ * @return array
+ */
+ public function getProfile(): array
+ {
+ return $this->_profile;
+ }
+
+ /**
+ * Send an email using the specified content, template and layout
+ *
+ * @param string|array|null $content String with message or array with messages
+ * @return array
+ * @throws \BadMethodCallException
+ * @psalm-return array{headers: string, message: string}
+ */
+ public function send($content = null): array
+ {
+ if (is_array($content)) {
+ $content = implode("\n", $content) . "\n";
+ }
+
+ $this->render($content);
+
+ $transport = $this->getTransport();
+ if (!$transport) {
+ $msg = 'Cannot send email, transport was not defined. Did you call transport() or define ' .
+ ' a transport in the set profile?';
+ throw new BadMethodCallException($msg);
+ }
+ $contents = $transport->send($this->message);
+ $this->_logDelivery($contents);
+
+ return $contents;
+ }
+
+ /**
+ * Render email.
+ *
+ * @param string|array|null $content Content array or string
+ * @return void
+ */
+ public function render($content = null): void
+ {
+ if (is_array($content)) {
+ $content = implode("\n", $content) . "\n";
+ }
+
+ $this->message->setBody(
+ $this->getRenderer()->render(
+ (string)$content,
+ $this->message->getBodyTypes()
+ )
+ );
+ }
+
+ /**
+ * Get view builder.
+ *
+ * @return \Cake\View\ViewBuilder
+ */
+ public function viewBuilder(): ViewBuilder
+ {
+ return $this->getRenderer()->viewBuilder();
+ }
+
+ /**
+ * Get email renderer.
+ *
+ * @return \Cake\Mailer\Renderer
+ */
+ public function getRenderer(): Renderer
+ {
+ if ($this->renderer === null) {
+ $this->renderer = new Renderer();
+ }
+
+ return $this->renderer;
+ }
+
+ /**
+ * Set email renderer.
+ *
+ * @param \Cake\Mailer\Renderer $renderer Render instance.
+ * @return $this
+ */
+ public function setRenderer(Renderer $renderer)
+ {
+ $this->renderer = $renderer;
+
+ return $this;
+ }
+
+ /**
+ * Log the email message delivery.
+ *
+ * @param array $contents The content with 'headers' and 'message' keys.
+ * @return void
+ */
+ protected function _logDelivery(array $contents): void
+ {
+ if (empty($this->_profile['log'])) {
+ return;
+ }
+ $config = [
+ 'level' => 'debug',
+ 'scope' => 'email',
+ ];
+ if ($this->_profile['log'] !== true) {
+ if (!is_array($this->_profile['log'])) {
+ $this->_profile['log'] = ['level' => $this->_profile['log']];
+ }
+ $config = $this->_profile['log'] + $config;
+ }
+ Log::write(
+ $config['level'],
+ PHP_EOL . $this->flatten($contents['headers']) . PHP_EOL . PHP_EOL . $this->flatten($contents['message']),
+ $config['scope']
+ );
+ }
+
+ /**
+ * Converts given value to string
+ *
+ * @param string|array $value The value to convert
+ * @return string
+ */
+ protected function flatten($value): string
+ {
+ return is_array($value) ? implode(';', $value) : $value;
+ }
+
+ /**
+ * Static method to fast create an instance of \Cake\Mailer\Email
+ *
+ * @param string|array|null $to Address to send (see Cake\Mailer\Email::to()).
+ * If null, will try to use 'to' from transport config
+ * @param string|null $subject String of subject or null to use 'subject' from transport config
+ * @param string|array|null $message String with message or array with variables to be used in render
+ * @param string|array $config String to use Email delivery profile from app.php or array with configs
+ * @param bool $send Send the email or just return the instance pre-configured
+ * @return \Cake\Mailer\Email Instance of Cake\Mailer\Email
+ * @throws \InvalidArgumentException
+ */
+ public static function deliver(
+ $to = null,
+ ?string $subject = null,
+ $message = null,
+ $config = 'default',
+ bool $send = true
+ ) {
+ if (is_array($config) && !isset($config['transport'])) {
+ $config['transport'] = 'default';
+ }
+
+ $instance = new static($config);
+ if ($to !== null) {
+ $instance->getMessage()->setTo($to);
+ }
+ if ($subject !== null) {
+ $instance->getMessage()->setSubject($subject);
+ }
+ if (is_array($message)) {
+ $instance->setViewVars($message);
+ $message = null;
+ } elseif ($message === null) {
+ $config = $instance->getProfile();
+ if (array_key_exists('message', $config)) {
+ $message = $config['message'];
+ }
+ }
+
+ if ($send === true) {
+ $instance->send($message);
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Reset all the internal variables to be able to send out a new email.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ $this->message->reset();
+ if ($this->renderer !== null) {
+ $this->renderer->reset();
+ }
+ $this->_transport = null;
+ $this->_profile = [];
+
+ return $this;
+ }
+
+ /**
+ * Serializes the email object to a value that can be natively serialized and re-used
+ * to clone this email instance.
+ *
+ * @return array Serializable array of configuration properties.
+ * @throws \Exception When a view var object can not be properly serialized.
+ */
+ public function jsonSerialize(): array
+ {
+ $array = $this->message->jsonSerialize();
+ $array['viewConfig'] = $this->getRenderer()->viewBuilder()->jsonSerialize();
+
+ return $array;
+ }
+
+ /**
+ * Configures an email instance object from serialized config.
+ *
+ * @param array $config Email configuration array.
+ * @return $this Configured email instance.
+ */
+ public function createFromArray(array $config)
+ {
+ if (isset($config['viewConfig'])) {
+ $this->getRenderer()->viewBuilder()->createFromArray($config['viewConfig']);
+ unset($config['viewConfig']);
+ }
+
+ if ($this->message === null) {
+ $this->message = new $this->messageClass();
+ }
+ $this->message->createFromArray($config);
+
+ return $this;
+ }
+
+ /**
+ * Serializes the Email object.
+ *
+ * @return string
+ */
+ public function serialize(): string
+ {
+ $array = $this->jsonSerialize();
+ array_walk_recursive($array, function (&$item, $key): void {
+ if ($item instanceof SimpleXMLElement) {
+ $item = json_decode(json_encode((array)$item), true);
+ }
+ });
+
+ return serialize($array);
+ }
+
+ /**
+ * Unserializes the Email object.
+ *
+ * @param string $data Serialized string.
+ * @return void
+ */
+ public function unserialize($data): void
+ {
+ $this->createFromArray(unserialize($data));
+ }
+
+ /**
+ * Proxy all static method calls (for methods provided by StaticConfigTrait) to Mailer.
+ *
+ * @param string $name Method name.
+ * @param array $arguments Method argument.
+ * @return mixed
+ */
+ public static function __callStatic($name, $arguments)
+ {
+ return [Mailer::class, $name](...$arguments);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Exception/MissingActionException.php b/app/vendor/cakephp/cakephp/src/Mailer/Exception/MissingActionException.php
new file mode 100644
index 000000000..eafd5be0a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/Exception/MissingActionException.php
@@ -0,0 +1,28 @@
+setSubject('Reset Password')
+ * ->setTo($user->email)
+ * ->set(['token' => $user->token]);
+ * }
+ * }
+ * ```
+ *
+ * Is a trivial example but shows how a mailer could be declared.
+ *
+ * ## Sending Messages
+ *
+ * After you have defined some messages you will want to send them:
+ *
+ * ```
+ * $mailer = new UserMailer();
+ * $mailer->send('resetPassword', $user);
+ * ```
+ *
+ * ## Event Listener
+ *
+ * Mailers can also subscribe to application event allowing you to
+ * decouple email delivery from your application code. By re-declaring the
+ * `implementedEvents()` method you can define event handlers that can
+ * convert events into email. For example, if your application had a user
+ * registration event:
+ *
+ * ```
+ * public function implementedEvents(): array
+ * {
+ * return [
+ * 'Model.afterSave' => 'onRegistration',
+ * ];
+ * }
+ *
+ * public function onRegistration(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ * {
+ * if ($entity->isNew()) {
+ * $this->send('welcome', [$entity]);
+ * }
+ * }
+ * ```
+ *
+ * The onRegistration method converts the application event into a mailer method.
+ * Our mailer could either be registered in the application bootstrap, or
+ * in the Table class' initialize() hook.
+ *
+ * @method $this setTo($email, $name = null) Sets "to" address. {@see \Cake\Mailer\Message::setTo()}
+ * @method array getTo() Gets "to" address. {@see \Cake\Mailer\Message::getTo()}
+ * @method $this setFrom($email, $name = null) Sets "from" address. {@see \Cake\Mailer\Message::setFrom()}
+ * @method array getFrom() Gets "from" address. {@see \Cake\Mailer\Message::getFrom()}
+ * @method $this setSender($email, $name = null) Sets "sender" address. {@see \Cake\Mailer\Message::setSender()}
+ * @method array getSender() Gets "sender" address. {@see \Cake\Mailer\Message::getSender()}
+ * @method $this setReplyTo($email, $name = null) Sets "Reply-To" address. {@see \Cake\Mailer\Message::setReplyTo()}
+ * @method array getReplyTo() Gets "Reply-To" address. {@see \Cake\Mailer\Message::getReplyTo()}
+ * @method $this addReplyTo($email, $name = null) Add "Reply-To" address. {@see \Cake\Mailer\Message::addReplyTo()}
+ * @method $this setReadReceipt($email, $name = null) Sets Read Receipt (Disposition-Notification-To header).
+ * {@see \Cake\Mailer\Message::setReadReceipt()}
+ * @method array getReadReceipt() Gets Read Receipt (Disposition-Notification-To header).
+ * {@see \Cake\Mailer\Message::getReadReceipt()}
+ * @method $this setReturnPath($email, $name = null) Sets return path. {@see \Cake\Mailer\Message::setReturnPath()}
+ * @method array getReturnPath() Gets return path. {@see \Cake\Mailer\Message::getReturnPath()}
+ * @method $this addTo($email, $name = null) Add "To" address. {@see \Cake\Mailer\Message::addTo()}
+ * @method $this setCc($email, $name = null) Sets "cc" address. {@see \Cake\Mailer\Message::setCc()}
+ * @method array getCc() Gets "cc" address. {@see \Cake\Mailer\Message::getCc()}
+ * @method $this addCc($email, $name = null) Add "cc" address. {@see \Cake\Mailer\Message::addCc()}
+ * @method $this setBcc($email, $name = null) Sets "bcc" address. {@see \Cake\Mailer\Message::setBcc()}
+ * @method array getBcc() Gets "bcc" address. {@see \Cake\Mailer\Message::getBcc()}
+ * @method $this addBcc($email, $name = null) Add "bcc" address. {@see \Cake\Mailer\Message::addBcc()}
+ * @method $this setCharset($charset) Charset setter. {@see \Cake\Mailer\Message::setCharset()}
+ * @method string getCharset() Charset getter. {@see \Cake\Mailer\Message::getCharset()}
+ * @method $this setHeaderCharset($charset) HeaderCharset setter. {@see \Cake\Mailer\Message::setHeaderCharset()}
+ * @method string getHeaderCharset() HeaderCharset getter. {@see \Cake\Mailer\Message::getHeaderCharset()}
+ * @method $this setSubject($subject) Sets subject. {@see \Cake\Mailer\Message::setSubject()}
+ * @method string getSubject() Gets subject. {@see \Cake\Mailer\Message::getSubject()}
+ * @method $this setHeaders(array $headers) Sets headers for the message. {@see \Cake\Mailer\Message::setHeaders()}
+ * @method $this addHeaders(array $headers) Add header for the message. {@see \Cake\Mailer\Message::addHeaders()}
+ * @method $this getHeaders(array $include = []) Get list of headers. {@see \Cake\Mailer\Message::getHeaders()}
+ * @method $this setEmailFormat($format) Sets email format. {@see \Cake\Mailer\Message::getHeaders()}
+ * @method string getEmailFormat() Gets email format. {@see \Cake\Mailer\Message::getEmailFormat()}
+ * @method $this setMessageId($message) Sets message ID. {@see \Cake\Mailer\Message::setMessageId()}
+ * @method bool|string getMessageId() Gets message ID. {@see \Cake\Mailer\Message::getMessageId()}
+ * @method $this setDomain($domain) Sets domain. {@see \Cake\Mailer\Message::setDomain()}
+ * @method string getDomain() Gets domain. {@see \Cake\Mailer\Message::getDomain()}
+ * @method $this setAttachments($attachments) Add attachments to the email message. {@see \Cake\Mailer\Message::setAttachments()}
+ * @method array getAttachments() Gets attachments to the email message. {@see \Cake\Mailer\Message::getAttachments()}
+ * @method $this addAttachments($attachments) Add attachments. {@see \Cake\Mailer\Message::addAttachments()}
+ * @method string|array getBody(?string $type = null) Get generated message body as array.
+ * {@see \Cake\Mailer\Message::getBody()}
+ */
+class Mailer implements EventListenerInterface
+{
+ use ModelAwareTrait;
+ use StaticConfigTrait;
+
+ /**
+ * Mailer's name.
+ *
+ * @var string
+ */
+ public static $name;
+
+ /**
+ * The transport instance to use for sending mail.
+ *
+ * @var \Cake\Mailer\AbstractTransport|null
+ */
+ protected $transport;
+
+ /**
+ * Message class name.
+ *
+ * @var string
+ * @psalm-var class-string<\Cake\Mailer\Message>
+ */
+ protected $messageClass = Message::class;
+
+ /**
+ * Message instance.
+ *
+ * @var \Cake\Mailer\Message
+ */
+ protected $message;
+
+ /**
+ * Email Renderer
+ *
+ * @var \Cake\Mailer\Renderer|null
+ */
+ protected $renderer;
+
+ /**
+ * Hold message, renderer and transport instance for restoring after runnning
+ * a mailer action.
+ *
+ * @var array
+ */
+ protected $clonedInstances = [
+ 'message' => null,
+ 'renderer' => null,
+ 'transport' => null,
+ ];
+
+ /**
+ * Mailer driver class map.
+ *
+ * @var array
+ * @psalm-var array
+ */
+ protected static $_dsnClassMap = [];
+
+ /**
+ * @var array|null
+ */
+ protected $logConfig = null;
+
+ /**
+ * Constructor
+ *
+ * @param array|string|null $config Array of configs, or string to load configs from app.php
+ */
+ public function __construct($config = null)
+ {
+ $this->message = new $this->messageClass();
+
+ if ($config === null) {
+ $config = static::getConfig('default');
+ }
+
+ if ($config) {
+ $this->setProfile($config);
+ }
+ }
+
+ /**
+ * Get the view builder.
+ *
+ * @return \Cake\View\ViewBuilder
+ */
+ public function viewBuilder(): ViewBuilder
+ {
+ return $this->getRenderer()->viewBuilder();
+ }
+
+ /**
+ * Get email renderer.
+ *
+ * @return \Cake\Mailer\Renderer
+ */
+ public function getRenderer(): Renderer
+ {
+ if ($this->renderer === null) {
+ $this->renderer = new Renderer();
+ }
+
+ return $this->renderer;
+ }
+
+ /**
+ * Set email renderer.
+ *
+ * @param \Cake\Mailer\Renderer $renderer Render instance.
+ * @return $this
+ */
+ public function setRenderer(Renderer $renderer)
+ {
+ $this->renderer = $renderer;
+
+ return $this;
+ }
+
+ /**
+ * Get message instance.
+ *
+ * @return \Cake\Mailer\Message
+ */
+ public function getMessage(): Message
+ {
+ return $this->message;
+ }
+
+ /**
+ * Set message instance.
+ *
+ * @param \Cake\Mailer\Message $message Message instance.
+ * @return $this
+ */
+ public function setMessage(Message $message)
+ {
+ $this->message = $message;
+
+ return $this;
+ }
+
+ /**
+ * Magic method to forward method class to Message instance.
+ *
+ * @param string $method Method name.
+ * @param array $args Method arguments
+ * @return $this|mixed
+ */
+ public function __call(string $method, array $args)
+ {
+ $result = $this->message->$method(...$args);
+ if (strpos($method, 'get') === 0) {
+ return $result;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets email view vars.
+ *
+ * @param string|array $key Variable name or hash of view variables.
+ * @param mixed $value View variable value.
+ * @return $this
+ * @deprecated 4.0.0 Use {@link Mailer::setViewVars()} instead.
+ */
+ public function set($key, $value = null)
+ {
+ deprecationWarning('Mailer::set() is deprecated. Use setViewVars() instead.');
+
+ return $this->setViewVars($key, $value);
+ }
+
+ /**
+ * Sets email view vars.
+ *
+ * @param string|array $key Variable name or hash of view variables.
+ * @param mixed $value View variable value.
+ * @return $this
+ */
+ public function setViewVars($key, $value = null)
+ {
+ $this->getRenderer()->set($key, $value);
+
+ return $this;
+ }
+
+ /**
+ * Sends email.
+ *
+ * @param string|null $action The name of the mailer action to trigger.
+ * If no action is specified then all other method arguments will be ignored.
+ * @param array $args Arguments to pass to the triggered mailer action.
+ * @param array $headers Headers to set.
+ * @return array
+ * @throws \Cake\Mailer\Exception\MissingActionException
+ * @throws \BadMethodCallException
+ * @psalm-return array{headers: string, message: string}
+ */
+ public function send(?string $action = null, array $args = [], array $headers = []): array
+ {
+ if ($action === null) {
+ return $this->deliver();
+ }
+
+ if (!method_exists($this, $action)) {
+ throw new MissingActionException([
+ 'mailer' => static::class,
+ 'action' => $action,
+ ]);
+ }
+
+ $this->clonedInstances['message'] = clone $this->message;
+ $this->clonedInstances['renderer'] = clone $this->getRenderer();
+ if ($this->transport !== null) {
+ $this->clonedInstances['transport'] = clone $this->transport;
+ }
+
+ $this->getMessage()->setHeaders($headers);
+ if (!$this->viewBuilder()->getTemplate()) {
+ $this->viewBuilder()->setTemplate($action);
+ }
+
+ try {
+ $this->$action(...$args);
+
+ $result = $this->deliver();
+ } finally {
+ $this->restore();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Render content and set message body.
+ *
+ * @param string $content Content.
+ * @return $this
+ */
+ public function render(string $content = '')
+ {
+ $content = $this->getRenderer()->render(
+ $content,
+ $this->message->getBodyTypes()
+ );
+
+ $this->message->setBody($content);
+
+ return $this;
+ }
+
+ /**
+ * Render content and send email using configured transport.
+ *
+ * @param string $content Content.
+ * @return array
+ * @psalm-return array{headers: string, message: string}
+ */
+ public function deliver(string $content = '')
+ {
+ $this->render($content);
+
+ $result = $this->getTransport()->send($this->message);
+ $this->logDelivery($result);
+
+ return $result;
+ }
+
+ /**
+ * Sets the configuration profile to use for this instance.
+ *
+ * @param string|array $config String with configuration name, or
+ * an array with config.
+ * @return $this
+ */
+ public function setProfile($config)
+ {
+ if (is_string($config)) {
+ $name = $config;
+ $config = static::getConfig($name);
+ if (empty($config)) {
+ throw new InvalidArgumentException(sprintf('Unknown email configuration "%s".', $name));
+ }
+ unset($name);
+ }
+
+ $simpleMethods = [
+ 'transport',
+ ];
+ foreach ($simpleMethods as $method) {
+ if (isset($config[$method])) {
+ $this->{'set' . ucfirst($method)}($config[$method]);
+ unset($config[$method]);
+ }
+ }
+
+ $viewBuilderMethods = [
+ 'template', 'layout', 'theme',
+ ];
+ foreach ($viewBuilderMethods as $method) {
+ if (array_key_exists($method, $config)) {
+ $this->viewBuilder()->{'set' . ucfirst($method)}($config[$method]);
+ unset($config[$method]);
+ }
+ }
+
+ if (array_key_exists('helpers', $config)) {
+ $this->viewBuilder()->setHelpers($config['helpers'], false);
+ unset($config['helpers']);
+ }
+ if (array_key_exists('viewRenderer', $config)) {
+ $this->viewBuilder()->setClassName($config['viewRenderer']);
+ unset($config['viewRenderer']);
+ }
+ if (array_key_exists('viewVars', $config)) {
+ $this->viewBuilder()->setVars($config['viewVars']);
+ unset($config['viewVars']);
+ }
+
+ if (isset($config['log'])) {
+ $this->setLogConfig($config['log']);
+ }
+
+ $this->message->setConfig($config);
+
+ return $this;
+ }
+
+ /**
+ * Sets the transport.
+ *
+ * When setting the transport you can either use the name
+ * of a configured transport or supply a constructed transport.
+ *
+ * @param string|\Cake\Mailer\AbstractTransport $name Either the name of a configured
+ * transport, or a transport instance.
+ * @return $this
+ * @throws \LogicException When the chosen transport lacks a send method.
+ * @throws \InvalidArgumentException When $name is neither a string nor an object.
+ */
+ public function setTransport($name)
+ {
+ if (is_string($name)) {
+ $transport = TransportFactory::get($name);
+ } elseif (is_object($name)) {
+ $transport = $name;
+ if (!$transport instanceof AbstractTransport) {
+ throw new CakeException('Transport class must extend Cake\Mailer\AbstractTransport');
+ }
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'The value passed for the "$name" argument must be either a string, or an object, %s given.',
+ gettype($name)
+ ));
+ }
+
+ $this->transport = $transport;
+
+ return $this;
+ }
+
+ /**
+ * Gets the transport.
+ *
+ * @return \Cake\Mailer\AbstractTransport
+ */
+ public function getTransport(): AbstractTransport
+ {
+ if ($this->transport === null) {
+ throw new BadMethodCallException(
+ 'Transport was not defined. '
+ . 'You must set on using setTransport() or set `transport` option in your mailer profile.'
+ );
+ }
+
+ return $this->transport;
+ }
+
+ /**
+ * Restore message, renderer, transport instances to state before an action was run.
+ *
+ * @return $this
+ */
+ protected function restore()
+ {
+ foreach (array_keys($this->clonedInstances) as $key) {
+ if ($this->clonedInstances[$key] === null) {
+ $this->{$key} = null;
+ } else {
+ $this->{$key} = clone $this->clonedInstances[$key];
+ $this->clonedInstances[$key] = null;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Reset all the internal variables to be able to send out a new email.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ $this->message->reset();
+ $this->getRenderer()->reset();
+ $this->transport = null;
+ $this->clonedInstances = [
+ 'message' => null,
+ 'renderer' => null,
+ 'transport' => null,
+ ];
+
+ return $this;
+ }
+
+ /**
+ * Log the email message delivery.
+ *
+ * @param array $contents The content with 'headers' and 'message' keys.
+ * @return void
+ * @psalm-param array{headers: string, message: string} $contents
+ */
+ protected function logDelivery(array $contents): void
+ {
+ if (empty($this->logConfig)) {
+ return;
+ }
+
+ Log::write(
+ $this->logConfig['level'],
+ PHP_EOL . $this->flatten($contents['headers']) . PHP_EOL . PHP_EOL . $this->flatten($contents['message']),
+ $this->logConfig['scope']
+ );
+ }
+
+ /**
+ * Set logging config.
+ *
+ * @param string|array|true $log Log config.
+ * @return void
+ */
+ protected function setLogConfig($log)
+ {
+ $config = [
+ 'level' => 'debug',
+ 'scope' => 'email',
+ ];
+ if ($log !== true) {
+ if (!is_array($log)) {
+ $log = ['level' => $log];
+ }
+ $config = $log + $config;
+ }
+
+ $this->logConfig = $config;
+ }
+
+ /**
+ * Converts given value to string
+ *
+ * @param string|array $value The value to convert
+ * @return string
+ */
+ protected function flatten($value): string
+ {
+ return is_array($value) ? implode(';', $value) : $value;
+ }
+
+ /**
+ * Implemented events.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ return [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/MailerAwareTrait.php b/app/vendor/cakephp/cakephp/src/Mailer/MailerAwareTrait.php
new file mode 100644
index 000000000..19b43b5f6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/MailerAwareTrait.php
@@ -0,0 +1,48 @@
+ 'ISO-2022-JP',
+ ];
+
+ /**
+ * Regex for email validation
+ *
+ * If null, filter_var() will be used. Use the emailPattern() method
+ * to set a custom pattern.'
+ *
+ * @var string|null
+ */
+ protected $emailPattern = self::EMAIL_PATTERN;
+
+ /**
+ * Constructor
+ *
+ * @param array|null $config Array of configs, or string to load configs from app.php
+ */
+ public function __construct(?array $config = null)
+ {
+ $this->appCharset = Configure::read('App.encoding');
+ if ($this->appCharset !== null) {
+ $this->charset = $this->appCharset;
+ }
+ $this->domain = preg_replace('/\:\d+$/', '', (string)env('HTTP_HOST'));
+ if (empty($this->domain)) {
+ $this->domain = php_uname('n');
+ }
+
+ if ($config) {
+ $this->setConfig($config);
+ }
+ }
+
+ /**
+ * Sets "from" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setFrom($email, ?string $name = null)
+ {
+ return $this->setEmailSingle('from', $email, $name, 'From requires only 1 email address.');
+ }
+
+ /**
+ * Gets "from" address.
+ *
+ * @return array
+ */
+ public function getFrom(): array
+ {
+ return $this->from;
+ }
+
+ /**
+ * Sets the "sender" address. See RFC link below for full explanation.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ * @link https://tools.ietf.org/html/rfc2822.html#section-3.6.2
+ */
+ public function setSender($email, ?string $name = null)
+ {
+ return $this->setEmailSingle('sender', $email, $name, 'Sender requires only 1 email address.');
+ }
+
+ /**
+ * Gets the "sender" address. See RFC link below for full explanation.
+ *
+ * @return array
+ * @link https://tools.ietf.org/html/rfc2822.html#section-3.6.2
+ */
+ public function getSender(): array
+ {
+ return $this->sender;
+ }
+
+ /**
+ * Sets "Reply-To" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setReplyTo($email, ?string $name = null)
+ {
+ return $this->setEmail('replyTo', $email, $name);
+ }
+
+ /**
+ * Gets "Reply-To" address.
+ *
+ * @return array
+ */
+ public function getReplyTo(): array
+ {
+ return $this->replyTo;
+ }
+
+ /**
+ * Add "Reply-To" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function addReplyTo($email, ?string $name = null)
+ {
+ return $this->addEmail('replyTo', $email, $name);
+ }
+
+ /**
+ * Sets Read Receipt (Disposition-Notification-To header).
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setReadReceipt($email, ?string $name = null)
+ {
+ return $this->setEmailSingle(
+ 'readReceipt',
+ $email,
+ $name,
+ 'Disposition-Notification-To requires only 1 email address.'
+ );
+ }
+
+ /**
+ * Gets Read Receipt (Disposition-Notification-To header).
+ *
+ * @return array
+ */
+ public function getReadReceipt(): array
+ {
+ return $this->readReceipt;
+ }
+
+ /**
+ * Sets return path.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setReturnPath($email, ?string $name = null)
+ {
+ return $this->setEmailSingle('returnPath', $email, $name, 'Return-Path requires only 1 email address.');
+ }
+
+ /**
+ * Gets return path.
+ *
+ * @return array
+ */
+ public function getReturnPath(): array
+ {
+ return $this->returnPath;
+ }
+
+ /**
+ * Sets "to" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function setTo($email, ?string $name = null)
+ {
+ return $this->setEmail('to', $email, $name);
+ }
+
+ /**
+ * Gets "to" address
+ *
+ * @return array
+ */
+ public function getTo(): array
+ {
+ return $this->to;
+ }
+
+ /**
+ * Add "To" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function addTo($email, ?string $name = null)
+ {
+ return $this->addEmail('to', $email, $name);
+ }
+
+ /**
+ * Sets "cc" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function setCc($email, ?string $name = null)
+ {
+ return $this->setEmail('cc', $email, $name);
+ }
+
+ /**
+ * Gets "cc" address.
+ *
+ * @return array
+ */
+ public function getCc(): array
+ {
+ return $this->cc;
+ }
+
+ /**
+ * Add "cc" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function addCc($email, ?string $name = null)
+ {
+ return $this->addEmail('cc', $email, $name);
+ }
+
+ /**
+ * Sets "bcc" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function setBcc($email, ?string $name = null)
+ {
+ return $this->setEmail('bcc', $email, $name);
+ }
+
+ /**
+ * Gets "bcc" address.
+ *
+ * @return array
+ */
+ public function getBcc(): array
+ {
+ return $this->bcc;
+ }
+
+ /**
+ * Add "bcc" address.
+ *
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ */
+ public function addBcc($email, ?string $name = null)
+ {
+ return $this->addEmail('bcc', $email, $name);
+ }
+
+ /**
+ * Charset setter.
+ *
+ * @param string $charset Character set.
+ * @return $this
+ */
+ public function setCharset(string $charset)
+ {
+ $this->charset = $charset;
+
+ return $this;
+ }
+
+ /**
+ * Charset getter.
+ *
+ * @return string Charset
+ */
+ public function getCharset(): string
+ {
+ return $this->charset;
+ }
+
+ /**
+ * HeaderCharset setter.
+ *
+ * @param string|null $charset Character set.
+ * @return $this
+ */
+ public function setHeaderCharset(?string $charset)
+ {
+ $this->headerCharset = $charset;
+
+ return $this;
+ }
+
+ /**
+ * HeaderCharset getter.
+ *
+ * @return string Charset
+ */
+ public function getHeaderCharset(): string
+ {
+ return $this->headerCharset ?: $this->charset;
+ }
+
+ /**
+ * TransferEncoding setter.
+ *
+ * @param string|null $encoding Encoding set.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setTransferEncoding(?string $encoding)
+ {
+ if ($encoding !== null) {
+ $encoding = strtolower($encoding);
+ if (!in_array($encoding, $this->transferEncodingAvailable, true)) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'Transfer encoding not available. Can be : %s.',
+ implode(', ', $this->transferEncodingAvailable)
+ )
+ );
+ }
+ }
+
+ $this->transferEncoding = $encoding;
+
+ return $this;
+ }
+
+ /**
+ * TransferEncoding getter.
+ *
+ * @return string|null Encoding
+ */
+ public function getTransferEncoding(): ?string
+ {
+ return $this->transferEncoding;
+ }
+
+ /**
+ * EmailPattern setter/getter
+ *
+ * @param string|null $regex The pattern to use for email address validation,
+ * null to unset the pattern and make use of filter_var() instead.
+ * @return $this
+ */
+ public function setEmailPattern(?string $regex)
+ {
+ $this->emailPattern = $regex;
+
+ return $this;
+ }
+
+ /**
+ * EmailPattern setter/getter
+ *
+ * @return string|null
+ */
+ public function getEmailPattern(): ?string
+ {
+ return $this->emailPattern;
+ }
+
+ /**
+ * Set email
+ *
+ * @param string $varName Property name
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ protected function setEmail(string $varName, $email, ?string $name)
+ {
+ if (!is_array($email)) {
+ $this->validateEmail($email, $varName);
+ $this->{$varName} = [$email => $name ?? $email];
+
+ return $this;
+ }
+ $list = [];
+ foreach ($email as $key => $value) {
+ if (is_int($key)) {
+ $key = $value;
+ }
+ $this->validateEmail($key, $varName);
+ $list[$key] = $value ?? $key;
+ }
+ $this->{$varName} = $list;
+
+ return $this;
+ }
+
+ /**
+ * Validate email address
+ *
+ * @param string $email Email address to validate
+ * @param string $context Which property was set
+ * @return void
+ * @throws \InvalidArgumentException If email address does not validate
+ */
+ protected function validateEmail(string $email, string $context): void
+ {
+ if ($this->emailPattern === null) {
+ if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ return;
+ }
+ } elseif (preg_match($this->emailPattern, $email)) {
+ return;
+ }
+
+ $context = ltrim($context, '_');
+ if ($email === '') {
+ throw new InvalidArgumentException(sprintf('The email set for "%s" is empty.', $context));
+ }
+ throw new InvalidArgumentException(sprintf('Invalid email set for "%s". You passed "%s".', $context, $email));
+ }
+
+ /**
+ * Set only 1 email
+ *
+ * @param string $varName Property name
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @param string $throwMessage Exception message
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ protected function setEmailSingle(string $varName, $email, ?string $name, string $throwMessage)
+ {
+ if ($email === []) {
+ $this->{$varName} = $email;
+
+ return $this;
+ }
+
+ $current = $this->{$varName};
+ $this->setEmail($varName, $email, $name);
+ if (count($this->{$varName}) !== 1) {
+ $this->{$varName} = $current;
+ throw new InvalidArgumentException($throwMessage);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add email
+ *
+ * @param string $varName Property name
+ * @param string|array $email String with email,
+ * Array with email as key, name as value or email as value (without name)
+ * @param string|null $name Name
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ protected function addEmail(string $varName, $email, ?string $name)
+ {
+ if (!is_array($email)) {
+ $this->validateEmail($email, $varName);
+ if ($name === null) {
+ $name = $email;
+ }
+ $this->{$varName}[$email] = $name;
+
+ return $this;
+ }
+ $list = [];
+ foreach ($email as $key => $value) {
+ if (is_int($key)) {
+ $key = $value;
+ }
+ $this->validateEmail($key, $varName);
+ $list[$key] = $value;
+ }
+ $this->{$varName} = array_merge($this->{$varName}, $list);
+
+ return $this;
+ }
+
+ /**
+ * Sets subject.
+ *
+ * @param string $subject Subject string.
+ * @return $this
+ */
+ public function setSubject(string $subject)
+ {
+ $this->subject = $this->encodeForHeader($subject);
+
+ return $this;
+ }
+
+ /**
+ * Gets subject.
+ *
+ * @return string
+ */
+ public function getSubject(): string
+ {
+ return $this->subject;
+ }
+
+ /**
+ * Get original subject without encoding
+ *
+ * @return string Original subject
+ */
+ public function getOriginalSubject(): string
+ {
+ return $this->decodeForHeader($this->subject);
+ }
+
+ /**
+ * Sets headers for the message
+ *
+ * @param array $headers Associative array containing headers to be set.
+ * @return $this
+ */
+ public function setHeaders(array $headers)
+ {
+ $this->headers = $headers;
+
+ return $this;
+ }
+
+ /**
+ * Add header for the message
+ *
+ * @param array $headers Headers to set.
+ * @return $this
+ */
+ public function addHeaders(array $headers)
+ {
+ $this->headers = Hash::merge($this->headers, $headers);
+
+ return $this;
+ }
+
+ /**
+ * Get list of headers
+ *
+ * ### Includes:
+ *
+ * - `from`
+ * - `replyTo`
+ * - `readReceipt`
+ * - `returnPath`
+ * - `to`
+ * - `cc`
+ * - `bcc`
+ * - `subject`
+ *
+ * @param string[] $include List of headers.
+ * @return string[]
+ */
+ public function getHeaders(array $include = []): array
+ {
+ $this->createBoundary();
+
+ if ($include === array_values($include)) {
+ $include = array_fill_keys($include, true);
+ }
+ $defaults = array_fill_keys(
+ [
+ 'from', 'sender', 'replyTo', 'readReceipt', 'returnPath',
+ 'to', 'cc', 'bcc', 'subject',
+ ],
+ false
+ );
+ $include += $defaults;
+
+ $headers = [];
+ $relation = [
+ 'from' => 'From',
+ 'replyTo' => 'Reply-To',
+ 'readReceipt' => 'Disposition-Notification-To',
+ 'returnPath' => 'Return-Path',
+ 'to' => 'To',
+ 'cc' => 'Cc',
+ 'bcc' => 'Bcc',
+ ];
+ $headersMultipleEmails = ['to', 'cc', 'bcc', 'replyTo'];
+ foreach ($relation as $var => $header) {
+ if ($include[$var]) {
+ if (in_array($var, $headersMultipleEmails)) {
+ $headers[$header] = implode(', ', $this->formatAddress($this->{$var}));
+ } else {
+ $headers[$header] = (string)current($this->formatAddress($this->{$var}));
+ }
+ }
+ }
+ if ($include['sender']) {
+ if (key($this->sender) === key($this->from)) {
+ $headers['Sender'] = '';
+ } else {
+ $headers['Sender'] = (string)current($this->formatAddress($this->sender));
+ }
+ }
+
+ $headers += $this->headers;
+ if (!isset($headers['Date'])) {
+ $headers['Date'] = date(DATE_RFC2822);
+ }
+ if ($this->messageId !== false) {
+ if ($this->messageId === true) {
+ $this->messageId = '<' . str_replace('-', '', Text::uuid()) . '@' . $this->domain . '>';
+ }
+
+ $headers['Message-ID'] = $this->messageId;
+ }
+
+ if ($this->priority) {
+ $headers['X-Priority'] = (string)$this->priority;
+ }
+
+ if ($include['subject']) {
+ $headers['Subject'] = $this->subject;
+ }
+
+ $headers['MIME-Version'] = '1.0';
+ if ($this->attachments) {
+ $headers['Content-Type'] = 'multipart/mixed; boundary="' . (string)$this->boundary . '"';
+ } elseif ($this->emailFormat === static::MESSAGE_BOTH) {
+ $headers['Content-Type'] = 'multipart/alternative; boundary="' . (string)$this->boundary . '"';
+ } elseif ($this->emailFormat === static::MESSAGE_TEXT) {
+ $headers['Content-Type'] = 'text/plain; charset=' . $this->getContentTypeCharset();
+ } elseif ($this->emailFormat === static::MESSAGE_HTML) {
+ $headers['Content-Type'] = 'text/html; charset=' . $this->getContentTypeCharset();
+ }
+ $headers['Content-Transfer-Encoding'] = $this->getContentTransferEncoding();
+
+ return $headers;
+ }
+
+ /**
+ * Get headers as string.
+ *
+ * @param string[] $include List of headers.
+ * @param string $eol End of line string for concatenating headers.
+ * @param \Closure $callback Callback to run each header value through before stringifying.
+ * @return string
+ * @see Message::getHeaders()
+ */
+ public function getHeadersString(array $include = [], string $eol = "\r\n", ?Closure $callback = null): string
+ {
+ $lines = $this->getHeaders($include);
+
+ if ($callback) {
+ $lines = array_map($callback, $lines);
+ }
+
+ $headers = [];
+ foreach ($lines as $key => $value) {
+ if (empty($value) && $value !== '0') {
+ continue;
+ }
+
+ foreach ((array)$value as $val) {
+ $headers[] = $key . ': ' . $val;
+ }
+ }
+
+ return implode($eol, $headers);
+ }
+
+ /**
+ * Format addresses
+ *
+ * If the address contains non alphanumeric/whitespace characters, it will
+ * be quoted as characters like `:` and `,` are known to cause issues
+ * in address header fields.
+ *
+ * @param array $address Addresses to format.
+ * @return array
+ */
+ protected function formatAddress(array $address): array
+ {
+ $return = [];
+ foreach ($address as $email => $alias) {
+ if ($email === $alias) {
+ $return[] = $email;
+ } else {
+ $encoded = $this->encodeForHeader($alias);
+ if ($encoded === $alias && preg_match('/[^a-z0-9 ]/i', $encoded)) {
+ $encoded = '"' . str_replace('"', '\"', $encoded) . '"';
+ }
+ $return[] = sprintf('%s <%s>', $encoded, $email);
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Sets email format.
+ *
+ * @param string $format Formatting string.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setEmailFormat(string $format)
+ {
+ if (!in_array($format, $this->emailFormatAvailable, true)) {
+ throw new InvalidArgumentException('Format not available.');
+ }
+ $this->emailFormat = $format;
+
+ return $this;
+ }
+
+ /**
+ * Gets email format.
+ *
+ * @return string
+ */
+ public function getEmailFormat(): string
+ {
+ return $this->emailFormat;
+ }
+
+ /**
+ * Gets the body types that are in this email message
+ *
+ * @return array Array of types. Valid types are Email::MESSAGE_TEXT and Email::MESSAGE_HTML
+ */
+ public function getBodyTypes(): array
+ {
+ $format = $this->emailFormat;
+
+ if ($format === static::MESSAGE_BOTH) {
+ return [static::MESSAGE_HTML, static::MESSAGE_TEXT];
+ }
+
+ return [$format];
+ }
+
+ /**
+ * Sets message ID.
+ *
+ * @param bool|string $message True to generate a new Message-ID, False to ignore (not send in email),
+ * String to set as Message-ID.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setMessageId($message)
+ {
+ if (is_bool($message)) {
+ $this->messageId = $message;
+ } else {
+ if (!preg_match('/^\<.+@.+\>$/', $message)) {
+ throw new InvalidArgumentException(
+ 'Invalid format to Message-ID. The text should be something like ""'
+ );
+ }
+ $this->messageId = $message;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets message ID.
+ *
+ * @return bool|string
+ */
+ public function getMessageId()
+ {
+ return $this->messageId;
+ }
+
+ /**
+ * Sets domain.
+ *
+ * Domain as top level (the part after @).
+ *
+ * @param string $domain Manually set the domain for CLI mailing.
+ * @return $this
+ */
+ public function setDomain(string $domain)
+ {
+ $this->domain = $domain;
+
+ return $this;
+ }
+
+ /**
+ * Gets domain.
+ *
+ * @return string
+ */
+ public function getDomain(): string
+ {
+ return $this->domain;
+ }
+
+ /**
+ * Add attachments to the email message
+ *
+ * Attachments can be defined in a few forms depending on how much control you need:
+ *
+ * Attach a single file:
+ *
+ * ```
+ * $this->setAttachments('path/to/file');
+ * ```
+ *
+ * Attach a file with a different filename:
+ *
+ * ```
+ * $this->setAttachments(['custom_name.txt' => 'path/to/file.txt']);
+ * ```
+ *
+ * Attach a file and specify additional properties:
+ *
+ * ```
+ * $this->setAttachments(['custom_name.png' => [
+ * 'file' => 'path/to/file',
+ * 'mimetype' => 'image/png',
+ * 'contentId' => 'abc123',
+ * 'contentDisposition' => false
+ * ]
+ * ]);
+ * ```
+ *
+ * Attach a file from string and specify additional properties:
+ *
+ * ```
+ * $this->setAttachments(['custom_name.png' => [
+ * 'data' => file_get_contents('path/to/file'),
+ * 'mimetype' => 'image/png'
+ * ]
+ * ]);
+ * ```
+ *
+ * The `contentId` key allows you to specify an inline attachment. In your email text, you
+ * can use `
` to display the image inline.
+ *
+ * The `contentDisposition` key allows you to disable the `Content-Disposition` header, this can improve
+ * attachment compatibility with outlook email clients.
+ *
+ * @param array $attachments Array of filenames.
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setAttachments(array $attachments)
+ {
+ $attach = [];
+ foreach ($attachments as $name => $fileInfo) {
+ if (!is_array($fileInfo)) {
+ $fileInfo = ['file' => $fileInfo];
+ }
+ if (!isset($fileInfo['file'])) {
+ if (!isset($fileInfo['data'])) {
+ throw new InvalidArgumentException('No file or data specified.');
+ }
+ if (is_int($name)) {
+ throw new InvalidArgumentException('No filename specified.');
+ }
+ $fileInfo['data'] = chunk_split(base64_encode($fileInfo['data']), 76, "\r\n");
+ } elseif ($fileInfo['file'] instanceof UploadedFileInterface) {
+ $fileInfo['mimetype'] = $fileInfo['file']->getClientMediaType();
+ if (is_int($name)) {
+ /** @var string $name */
+ $name = $fileInfo['file']->getClientFilename();
+ }
+ } elseif (is_string($fileInfo['file'])) {
+ $fileName = $fileInfo['file'];
+ $fileInfo['file'] = realpath($fileInfo['file']);
+ if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) {
+ throw new InvalidArgumentException(sprintf('File not found: "%s"', $fileName));
+ }
+ if (is_int($name)) {
+ $name = basename($fileInfo['file']);
+ }
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'File must be a filepath or UploadedFileInterface instance. Found `%s` instead.',
+ gettype($fileInfo['file'])
+ ));
+ }
+ if (
+ !isset($fileInfo['mimetype'])
+ && isset($fileInfo['file'])
+ && is_string($fileInfo['file'])
+ && function_exists('mime_content_type')
+ ) {
+ $fileInfo['mimetype'] = mime_content_type($fileInfo['file']);
+ }
+ if (!isset($fileInfo['mimetype'])) {
+ $fileInfo['mimetype'] = 'application/octet-stream';
+ }
+ $attach[$name] = $fileInfo;
+ }
+ $this->attachments = $attach;
+
+ return $this;
+ }
+
+ /**
+ * Gets attachments to the email message.
+ *
+ * @return array Array of attachments.
+ */
+ public function getAttachments(): array
+ {
+ return $this->attachments;
+ }
+
+ /**
+ * Add attachments
+ *
+ * @param array $attachments Array of filenames.
+ * @return $this
+ * @throws \InvalidArgumentException
+ * @see \Cake\Mailer\Email::setAttachments()
+ */
+ public function addAttachments(array $attachments)
+ {
+ $current = $this->attachments;
+ $this->setAttachments($attachments);
+ $this->attachments = array_merge($current, $this->attachments);
+
+ return $this;
+ }
+
+ /**
+ * Get generated message body as array.
+ *
+ * @return array
+ */
+ public function getBody()
+ {
+ if (empty($this->message)) {
+ $this->message = $this->generateMessage();
+ }
+
+ return $this->message;
+ }
+
+ /**
+ * Get generated body as string.
+ *
+ * @param string $eol End of line string for imploding.
+ * @return string
+ * @see Message::getBody()
+ */
+ public function getBodyString(string $eol = "\r\n"): string
+ {
+ $lines = $this->getBody();
+
+ return implode($eol, $lines);
+ }
+
+ /**
+ * Create unique boundary identifier
+ *
+ * @return void
+ */
+ protected function createBoundary(): void
+ {
+ if (
+ $this->boundary === null &&
+ (
+ $this->attachments ||
+ $this->emailFormat === static::MESSAGE_BOTH
+ )
+ ) {
+ $this->boundary = md5(Security::randomBytes(16));
+ }
+ }
+
+ /**
+ * Generate full message.
+ *
+ * @return string[]
+ */
+ protected function generateMessage(): array
+ {
+ $this->createBoundary();
+ $msg = [];
+
+ $contentIds = array_filter((array)Hash::extract($this->attachments, '{s}.contentId'));
+ $hasInlineAttachments = count($contentIds) > 0;
+ $hasAttachments = !empty($this->attachments);
+ $hasMultipleTypes = $this->emailFormat === static::MESSAGE_BOTH;
+ $multiPart = ($hasAttachments || $hasMultipleTypes);
+
+ /** @var string $boundary */
+ $boundary = $this->boundary;
+ $relBoundary = $textBoundary = $boundary;
+
+ if ($hasInlineAttachments) {
+ $msg[] = '--' . $boundary;
+ $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"';
+ $msg[] = '';
+ $relBoundary = $textBoundary = 'rel-' . $boundary;
+ }
+
+ if ($hasMultipleTypes && $hasAttachments) {
+ $msg[] = '--' . $relBoundary;
+ $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"';
+ $msg[] = '';
+ $textBoundary = 'alt-' . $boundary;
+ }
+
+ if (
+ $this->emailFormat === static::MESSAGE_TEXT
+ || $this->emailFormat === static::MESSAGE_BOTH
+ ) {
+ if ($multiPart) {
+ $msg[] = '--' . $textBoundary;
+ $msg[] = 'Content-Type: text/plain; charset=' . $this->getContentTypeCharset();
+ $msg[] = 'Content-Transfer-Encoding: ' . $this->getContentTransferEncoding();
+ $msg[] = '';
+ }
+ $content = explode("\n", $this->textMessage);
+ $msg = array_merge($msg, $content);
+ $msg[] = '';
+ $msg[] = '';
+ }
+
+ if (
+ $this->emailFormat === static::MESSAGE_HTML
+ || $this->emailFormat === static::MESSAGE_BOTH
+ ) {
+ if ($multiPart) {
+ $msg[] = '--' . $textBoundary;
+ $msg[] = 'Content-Type: text/html; charset=' . $this->getContentTypeCharset();
+ $msg[] = 'Content-Transfer-Encoding: ' . $this->getContentTransferEncoding();
+ $msg[] = '';
+ }
+ $content = explode("\n", $this->htmlMessage);
+ $msg = array_merge($msg, $content);
+ $msg[] = '';
+ $msg[] = '';
+ }
+
+ if ($textBoundary !== $relBoundary) {
+ $msg[] = '--' . $textBoundary . '--';
+ $msg[] = '';
+ }
+
+ if ($hasInlineAttachments) {
+ $attachments = $this->attachInlineFiles($relBoundary);
+ $msg = array_merge($msg, $attachments);
+ $msg[] = '';
+ $msg[] = '--' . $relBoundary . '--';
+ $msg[] = '';
+ }
+
+ if ($hasAttachments) {
+ $attachments = $this->attachFiles($boundary);
+ $msg = array_merge($msg, $attachments);
+ }
+ if ($hasAttachments || $hasMultipleTypes) {
+ $msg[] = '';
+ $msg[] = '--' . $boundary . '--';
+ $msg[] = '';
+ }
+
+ return $msg;
+ }
+
+ /**
+ * Attach non-embedded files by adding file contents inside boundaries.
+ *
+ * @param string|null $boundary Boundary to use. If null, will default to $this->boundary
+ * @return string[] An array of lines to add to the message
+ */
+ protected function attachFiles(?string $boundary = null): array
+ {
+ if ($boundary === null) {
+ /** @var string $boundary */
+ $boundary = $this->boundary;
+ }
+
+ $msg = [];
+ foreach ($this->attachments as $filename => $fileInfo) {
+ if (!empty($fileInfo['contentId'])) {
+ continue;
+ }
+ $data = $fileInfo['data'] ?? $this->readFile($fileInfo['file']);
+ $hasDisposition = (
+ !isset($fileInfo['contentDisposition']) ||
+ $fileInfo['contentDisposition']
+ );
+ $part = new FormDataPart('', $data, '', $this->getHeaderCharset());
+
+ if ($hasDisposition) {
+ $part->disposition('attachment');
+ $part->filename($filename);
+ }
+ $part->transferEncoding('base64');
+ $part->type($fileInfo['mimetype']);
+
+ $msg[] = '--' . $boundary;
+ $msg[] = (string)$part;
+ $msg[] = '';
+ }
+
+ return $msg;
+ }
+
+ /**
+ * Attach inline/embedded files to the message.
+ *
+ * @param string|null $boundary Boundary to use. If null, will default to $this->boundary
+ * @return string[] An array of lines to add to the message
+ */
+ protected function attachInlineFiles(?string $boundary = null): array
+ {
+ if ($boundary === null) {
+ /** @var string $boundary */
+ $boundary = $this->boundary;
+ }
+
+ $msg = [];
+ foreach ($this->getAttachments() as $filename => $fileInfo) {
+ if (empty($fileInfo['contentId'])) {
+ continue;
+ }
+ $data = $fileInfo['data'] ?? $this->readFile($fileInfo['file']);
+
+ $msg[] = '--' . $boundary;
+ $part = new FormDataPart('', $data, 'inline', $this->getHeaderCharset());
+ $part->type($fileInfo['mimetype']);
+ $part->transferEncoding('base64');
+ $part->contentId($fileInfo['contentId']);
+ $part->filename($filename);
+ $msg[] = (string)$part;
+ $msg[] = '';
+ }
+
+ return $msg;
+ }
+
+ /**
+ * Sets priority.
+ *
+ * @param int|null $priority 1 (highest) to 5 (lowest)
+ * @return $this
+ */
+ public function setPriority(?int $priority)
+ {
+ $this->priority = $priority;
+
+ return $this;
+ }
+
+ /**
+ * Gets priority.
+ *
+ * @return int|null
+ */
+ public function getPriority(): ?int
+ {
+ return $this->priority;
+ }
+
+ /**
+ * Sets the configuration for this instance.
+ *
+ * @param array $config Config array.
+ * @return $this
+ */
+ public function setConfig(array $config)
+ {
+ $simpleMethods = [
+ 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath',
+ 'cc', 'bcc', 'messageId', 'domain', 'subject', 'attachments',
+ 'emailFormat', 'emailPattern', 'charset', 'headerCharset',
+ ];
+ foreach ($simpleMethods as $method) {
+ if (isset($config[$method])) {
+ $this->{'set' . ucfirst($method)}($config[$method]);
+ }
+ }
+
+ if (isset($config['headers'])) {
+ $this->setHeaders($config['headers']);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set message body.
+ *
+ * @param array $content Content array with keys "text" and/or "html" with
+ * content string of respective type.
+ * @return $this
+ */
+ public function setBody(array $content)
+ {
+ foreach ($content as $type => $text) {
+ if (!in_array($type, $this->emailFormatAvailable, true)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Invalid message type: "%s". Valid types are: "text", "html".',
+ $type
+ ));
+ }
+
+ $text = str_replace(["\r\n", "\r"], "\n", $text);
+ $text = $this->encodeString($text, $this->getCharset());
+ $text = $this->wrap($text);
+ $text = implode("\n", $text);
+ $text = rtrim($text, "\n");
+
+ $property = "{$type}Message";
+ $this->$property = $text;
+ }
+
+ $this->boundary = null;
+ $this->message = [];
+
+ return $this;
+ }
+
+ /**
+ * Set text body for message.
+ *
+ * @param string $content Content string
+ * @return $this
+ */
+ public function setBodyText(string $content)
+ {
+ $this->setBody([static::MESSAGE_TEXT => $content]);
+
+ return $this;
+ }
+
+ /**
+ * Set HTML body for message.
+ *
+ * @param string $content Content string
+ * @return $this
+ */
+ public function setBodyHtml(string $content)
+ {
+ $this->setBody([static::MESSAGE_HTML => $content]);
+
+ return $this;
+ }
+
+ /**
+ * Get text body of message.
+ *
+ * @return string
+ */
+ public function getBodyText()
+ {
+ return $this->textMessage;
+ }
+
+ /**
+ * Get HTML body of message.
+ *
+ * @return string
+ */
+ public function getBodyHtml()
+ {
+ return $this->htmlMessage;
+ }
+
+ /**
+ * Translates a string for one charset to another if the App.encoding value
+ * differs and the mb_convert_encoding function exists
+ *
+ * @param string $text The text to be converted
+ * @param string $charset the target encoding
+ * @return string
+ */
+ protected function encodeString(string $text, string $charset): string
+ {
+ if ($this->appCharset === $charset) {
+ return $text;
+ }
+
+ if ($this->appCharset === null) {
+ return mb_convert_encoding($text, $charset);
+ }
+
+ return mb_convert_encoding($text, $charset, $this->appCharset);
+ }
+
+ /**
+ * Wrap the message to follow the RFC 2822 - 2.1.1
+ *
+ * @param string|null $message Message to wrap
+ * @param int $wrapLength The line length
+ * @return array Wrapped message
+ */
+ protected function wrap(?string $message = null, int $wrapLength = self::LINE_LENGTH_MUST): array
+ {
+ if ($message === null || strlen($message) === 0) {
+ return [''];
+ }
+ $message = str_replace(["\r\n", "\r"], "\n", $message);
+ $lines = explode("\n", $message);
+ $formatted = [];
+ $cut = ($wrapLength === static::LINE_LENGTH_MUST);
+
+ foreach ($lines as $line) {
+ if (empty($line) && $line !== '0') {
+ $formatted[] = '';
+ continue;
+ }
+ if (strlen($line) < $wrapLength) {
+ $formatted[] = $line;
+ continue;
+ }
+ if (!preg_match('/<[a-z]+.*>/i', $line)) {
+ $formatted = array_merge(
+ $formatted,
+ explode("\n", Text::wordWrap($line, $wrapLength, "\n", $cut))
+ );
+ continue;
+ }
+
+ $tagOpen = false;
+ $tmpLine = $tag = '';
+ $tmpLineLength = 0;
+ for ($i = 0, $count = strlen($line); $i < $count; $i++) {
+ $char = $line[$i];
+ if ($tagOpen) {
+ $tag .= $char;
+ if ($char === '>') {
+ $tagLength = strlen($tag);
+ if ($tagLength + $tmpLineLength < $wrapLength) {
+ $tmpLine .= $tag;
+ $tmpLineLength += $tagLength;
+ } else {
+ if ($tmpLineLength > 0) {
+ $formatted = array_merge(
+ $formatted,
+ explode("\n", Text::wordWrap(trim($tmpLine), $wrapLength, "\n", $cut))
+ );
+ $tmpLine = '';
+ $tmpLineLength = 0;
+ }
+ if ($tagLength > $wrapLength) {
+ $formatted[] = $tag;
+ } else {
+ $tmpLine = $tag;
+ $tmpLineLength = $tagLength;
+ }
+ }
+ $tag = '';
+ $tagOpen = false;
+ }
+ continue;
+ }
+ if ($char === '<') {
+ $tagOpen = true;
+ $tag = '<';
+ continue;
+ }
+ if ($char === ' ' && $tmpLineLength >= $wrapLength) {
+ $formatted[] = $tmpLine;
+ $tmpLineLength = 0;
+ continue;
+ }
+ $tmpLine .= $char;
+ $tmpLineLength++;
+ if ($tmpLineLength === $wrapLength) {
+ $nextChar = $line[$i + 1] ?? '';
+ if ($nextChar === ' ' || $nextChar === '<') {
+ $formatted[] = trim($tmpLine);
+ $tmpLine = '';
+ $tmpLineLength = 0;
+ if ($nextChar === ' ') {
+ $i++;
+ }
+ } else {
+ $lastSpace = strrpos($tmpLine, ' ');
+ if ($lastSpace === false) {
+ continue;
+ }
+ $formatted[] = trim(substr($tmpLine, 0, $lastSpace));
+ $tmpLine = substr($tmpLine, $lastSpace + 1);
+
+ $tmpLineLength = strlen($tmpLine);
+ }
+ }
+ }
+ if (!empty($tmpLine)) {
+ $formatted[] = $tmpLine;
+ }
+ }
+ $formatted[] = '';
+
+ return $formatted;
+ }
+
+ /**
+ * Reset all the internal variables to be able to send out a new email.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ $this->to = [];
+ $this->from = [];
+ $this->sender = [];
+ $this->replyTo = [];
+ $this->readReceipt = [];
+ $this->returnPath = [];
+ $this->cc = [];
+ $this->bcc = [];
+ $this->messageId = true;
+ $this->subject = '';
+ $this->headers = [];
+ $this->textMessage = '';
+ $this->htmlMessage = '';
+ $this->message = [];
+ $this->emailFormat = static::MESSAGE_TEXT;
+ $this->priority = null;
+ $this->charset = 'utf-8';
+ $this->headerCharset = null;
+ $this->transferEncoding = null;
+ $this->attachments = [];
+ $this->emailPattern = static::EMAIL_PATTERN;
+
+ return $this;
+ }
+
+ /**
+ * Encode the specified string using the current charset
+ *
+ * @param string $text String to encode
+ * @return string Encoded string
+ */
+ protected function encodeForHeader(string $text): string
+ {
+ if ($this->appCharset === null) {
+ return $text;
+ }
+
+ /** @var string $restore */
+ $restore = mb_internal_encoding();
+ mb_internal_encoding($this->appCharset);
+ $return = mb_encode_mimeheader($text, $this->getHeaderCharset(), 'B');
+ mb_internal_encoding($restore);
+
+ return $return;
+ }
+
+ /**
+ * Decode the specified string
+ *
+ * @param string $text String to decode
+ * @return string Decoded string
+ */
+ protected function decodeForHeader(string $text): string
+ {
+ if ($this->appCharset === null) {
+ return $text;
+ }
+
+ /** @var string $restore */
+ $restore = mb_internal_encoding();
+ mb_internal_encoding($this->appCharset);
+ $return = mb_decode_mimeheader($text);
+ mb_internal_encoding($restore);
+
+ return $return;
+ }
+
+ /**
+ * Read the file contents and return a base64 version of the file contents.
+ *
+ * @param string|\Psr\Http\Message\UploadedFileInterface $file The absolute path to the file to read
+ * or UploadedFileInterface instance.
+ * @return string File contents in base64 encoding
+ */
+ protected function readFile($file): string
+ {
+ if (is_string($file)) {
+ $content = (string)file_get_contents($file);
+ } else {
+ $content = (string)$file->getStream();
+ }
+
+ return chunk_split(base64_encode($content));
+ }
+
+ /**
+ * Return the Content-Transfer Encoding value based
+ * on the set transferEncoding or set charset.
+ *
+ * @return string
+ */
+ public function getContentTransferEncoding(): string
+ {
+ if ($this->transferEncoding) {
+ return $this->transferEncoding;
+ }
+
+ $charset = strtoupper($this->charset);
+ if (in_array($charset, $this->charset8bit, true)) {
+ return '8bit';
+ }
+
+ return '7bit';
+ }
+
+ /**
+ * Return charset value for Content-Type.
+ *
+ * Checks fallback/compatibility types which include workarounds
+ * for legacy japanese character sets.
+ *
+ * @return string
+ */
+ public function getContentTypeCharset(): string
+ {
+ $charset = strtoupper($this->charset);
+ if (array_key_exists($charset, $this->contentTypeCharset)) {
+ return strtoupper($this->contentTypeCharset[$charset]);
+ }
+
+ return strtoupper($this->charset);
+ }
+
+ /**
+ * Serializes the email object to a value that can be natively serialized and re-used
+ * to clone this email instance.
+ *
+ * @return array Serializable array of configuration properties.
+ * @throws \Exception When a view var object can not be properly serialized.
+ */
+ public function jsonSerialize(): array
+ {
+ $properties = [
+ 'to', 'from', 'sender', 'replyTo', 'cc', 'bcc', 'subject',
+ 'returnPath', 'readReceipt', 'emailFormat', 'emailPattern', 'domain',
+ 'attachments', 'messageId', 'headers', 'appCharset', 'charset', 'headerCharset',
+ 'textMessage', 'htmlMessage',
+ ];
+
+ $array = [];
+ foreach ($properties as $property) {
+ $array[$property] = $this->{$property};
+ }
+
+ array_walk($array['attachments'], function (&$item, $key): void {
+ if (!empty($item['file'])) {
+ $item['data'] = $this->readFile($item['file']);
+ unset($item['file']);
+ }
+ });
+
+ return array_filter($array, function ($i) {
+ return $i !== null && !is_array($i) && !is_bool($i) && strlen($i) || !empty($i);
+ });
+ }
+
+ /**
+ * Configures an email instance object from serialized config.
+ *
+ * @param array $config Email configuration array.
+ * @return $this Configured email instance.
+ */
+ public function createFromArray(array $config)
+ {
+ foreach ($config as $property => $value) {
+ $this->{$property} = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Serializes the Email object.
+ *
+ * @return string
+ */
+ public function serialize(): string
+ {
+ $array = $this->jsonSerialize();
+ array_walk_recursive($array, function (&$item, $key): void {
+ if ($item instanceof SimpleXMLElement) {
+ $item = json_decode(json_encode((array)$item), true);
+ }
+ });
+
+ return serialize($array);
+ }
+
+ /**
+ * Unserializes the Message object.
+ *
+ * @param string $data Serialized string.
+ * @return void
+ */
+ public function unserialize($data)
+ {
+ $array = unserialize($data);
+ if (!is_array($array)) {
+ throw new CakeException('Unable to unserialize message.');
+ }
+
+ $this->createFromArray($array);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Renderer.php b/app/vendor/cakephp/cakephp/src/Mailer/Renderer.php
new file mode 100644
index 000000000..118b41f70
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/Renderer.php
@@ -0,0 +1,120 @@
+reset();
+ }
+
+ /**
+ * Render text/HTML content.
+ *
+ * If there is no template set, the $content will be returned in a hash
+ * of the specified content types for the email.
+ *
+ * @param string $content The content.
+ * @param string[] $types Content types to render. Valid array values are Message::MESSAGE_HTML, Message::MESSAGE_TEXT.
+ * @return array The rendered content with "html" and/or "text" keys.
+ * @psalm-param (\Cake\Mailer\Message::MESSAGE_HTML|\Cake\Mailer\Message::MESSAGE_TEXT)[] $types
+ * @psalm-return array{html?: string, text?: string}
+ */
+ public function render(string $content, array $types = []): array
+ {
+ $rendered = [];
+ $template = $this->viewBuilder()->getTemplate();
+ if (empty($template)) {
+ foreach ($types as $type) {
+ $rendered[$type] = $content;
+ }
+
+ return $rendered;
+ }
+
+ $view = $this->createView();
+
+ [$templatePlugin] = pluginSplit($view->getTemplate());
+ [$layoutPlugin] = pluginSplit($view->getLayout());
+ if ($templatePlugin) {
+ $view->setPlugin($templatePlugin);
+ } elseif ($layoutPlugin) {
+ $view->setPlugin($layoutPlugin);
+ }
+
+ if ($view->get('content') === null) {
+ $view->set('content', $content);
+ }
+
+ foreach ($types as $type) {
+ $view->setTemplatePath(static::TEMPLATE_FOLDER . DIRECTORY_SEPARATOR . $type);
+ $view->setLayoutPath(static::TEMPLATE_FOLDER . DIRECTORY_SEPARATOR . $type);
+
+ $rendered[$type] = $view->render();
+ }
+
+ return $rendered;
+ }
+
+ /**
+ * Reset view builder to defaults.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ $this->_viewBuilder = null;
+
+ $this->viewBuilder()
+ ->setClassName(View::class)
+ ->setLayout('default')
+ ->setHelpers(['Html'], false);
+
+ return $this;
+ }
+
+ /**
+ * Clone ViewBuilder instance when renderer is cloned.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ if ($this->_viewBuilder !== null) {
+ $this->_viewBuilder = clone $this->_viewBuilder;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Transport/DebugTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/Transport/DebugTransport.php
new file mode 100644
index 000000000..52671bd77
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/Transport/DebugTransport.php
@@ -0,0 +1,42 @@
+getHeadersString(
+ ['from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'subject']
+ );
+ $message = implode("\r\n", $message->getBody());
+
+ return ['headers' => $headers, 'message' => $message];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Transport/MailTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/Transport/MailTransport.php
new file mode 100644
index 000000000..4719c22ba
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/Transport/MailTransport.php
@@ -0,0 +1,98 @@
+checkRecipient($message);
+
+ // https://github.com/cakephp/cakephp/issues/2209
+ // https://bugs.php.net/bug.php?id=47983
+ $subject = str_replace("\r\n", '', $message->getSubject());
+
+ $to = $message->getHeaders(['to'])['To'];
+ $to = str_replace("\r\n", '', $to);
+
+ $eol = $this->getConfig('eol', PHP_EOL);
+ $headers = $message->getHeadersString(
+ [
+ 'from',
+ 'sender',
+ 'replyTo',
+ 'readReceipt',
+ 'returnPath',
+ 'cc',
+ 'bcc',
+ ],
+ $eol,
+ function ($val) {
+ return str_replace("\r\n", '', $val);
+ }
+ );
+
+ $message = $message->getBodyString($eol);
+
+ $params = $this->getConfig('additionalParameters', '');
+ $this->_mail($to, $subject, $message, $headers, $params);
+
+ $headers .= $eol . 'To: ' . $to;
+ $headers .= $eol . 'Subject: ' . $subject;
+
+ return ['headers' => $headers, 'message' => $message];
+ }
+
+ /**
+ * Wraps internal function mail() and throws exception instead of errors if anything goes wrong
+ *
+ * @param string $to email's recipient
+ * @param string $subject email's subject
+ * @param string $message email's body
+ * @param string $headers email's custom headers
+ * @param string $params additional params for sending email
+ * @throws \Cake\Network\Exception\SocketException if mail could not be sent
+ * @return void
+ */
+ protected function _mail(
+ string $to,
+ string $subject,
+ string $message,
+ string $headers = '',
+ string $params = ''
+ ): void {
+ // phpcs:disable
+ if (!@mail($to, $subject, $message, $headers, $params)) {
+ $error = error_get_last();
+ $msg = 'Could not send email: ' . ($error['message'] ?? 'unknown');
+ throw new CakeException($msg);
+ }
+ // phpcs:enable
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/Transport/SmtpTransport.php b/app/vendor/cakephp/cakephp/src/Mailer/Transport/SmtpTransport.php
new file mode 100644
index 000000000..2ea6dcd77
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/Transport/SmtpTransport.php
@@ -0,0 +1,544 @@
+ 'localhost',
+ 'port' => 25,
+ 'timeout' => 30,
+ 'username' => null,
+ 'password' => null,
+ 'client' => null,
+ 'tls' => false,
+ 'keepAlive' => false,
+ ];
+
+ /**
+ * Socket to SMTP server
+ *
+ * @var \Cake\Network\Socket|null
+ */
+ protected $_socket;
+
+ /**
+ * Content of email to return
+ *
+ * @var array
+ */
+ protected $_content = [];
+
+ /**
+ * The response of the last sent SMTP command.
+ *
+ * @var array
+ */
+ protected $_lastResponse = [];
+
+ /**
+ * Destructor
+ *
+ * Tries to disconnect to ensure that the connection is being
+ * terminated properly before the socket gets closed.
+ */
+ public function __destruct()
+ {
+ try {
+ $this->disconnect();
+ } catch (Exception $e) {
+ // avoid fatal error on script termination
+ }
+ }
+
+ /**
+ * Unserialize handler.
+ *
+ * Ensure that the socket property isn't reinitialized in a broken state.
+ *
+ * @return void
+ */
+ public function __wakeup(): void
+ {
+ $this->_socket = null;
+ }
+
+ /**
+ * Connect to the SMTP server.
+ *
+ * This method tries to connect only in case there is no open
+ * connection available already.
+ *
+ * @return void
+ */
+ public function connect(): void
+ {
+ if (!$this->connected()) {
+ $this->_connect();
+ $this->_auth();
+ }
+ }
+
+ /**
+ * Check whether an open connection to the SMTP server is available.
+ *
+ * @return bool
+ */
+ public function connected(): bool
+ {
+ return $this->_socket !== null && $this->_socket->connected;
+ }
+
+ /**
+ * Disconnect from the SMTP server.
+ *
+ * This method tries to disconnect only in case there is an open
+ * connection available.
+ *
+ * @return void
+ */
+ public function disconnect(): void
+ {
+ if (!$this->connected()) {
+ return;
+ }
+
+ $this->_disconnect();
+ }
+
+ /**
+ * Returns the response of the last sent SMTP command.
+ *
+ * A response consists of one or more lines containing a response
+ * code and an optional response message text:
+ * ```
+ * [
+ * [
+ * 'code' => '250',
+ * 'message' => 'mail.example.com'
+ * ],
+ * [
+ * 'code' => '250',
+ * 'message' => 'PIPELINING'
+ * ],
+ * [
+ * 'code' => '250',
+ * 'message' => '8BITMIME'
+ * ],
+ * // etc...
+ * ]
+ * ```
+ *
+ * @return array
+ */
+ public function getLastResponse(): array
+ {
+ return $this->_lastResponse;
+ }
+
+ /**
+ * Send mail
+ *
+ * @param \Cake\Mailer\Message $message Message instance
+ * @return array
+ * @throws \Cake\Network\Exception\SocketException
+ * @psalm-return array{headers: string, message: string}
+ */
+ public function send(Message $message): array
+ {
+ $this->checkRecipient($message);
+
+ if (!$this->connected()) {
+ $this->_connect();
+ $this->_auth();
+ } else {
+ $this->_smtpSend('RSET');
+ }
+
+ $this->_sendRcpt($message);
+ $this->_sendData($message);
+
+ if (!$this->_config['keepAlive']) {
+ $this->_disconnect();
+ }
+
+ return $this->_content;
+ }
+
+ /**
+ * Parses and stores the response lines in `'code' => 'message'` format.
+ *
+ * @param string[] $responseLines Response lines to parse.
+ * @return void
+ */
+ protected function _bufferResponseLines(array $responseLines): void
+ {
+ $response = [];
+ foreach ($responseLines as $responseLine) {
+ if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) {
+ $response[] = [
+ 'code' => $match[1],
+ 'message' => $match[2] ?? null,
+ ];
+ }
+ }
+ $this->_lastResponse = array_merge($this->_lastResponse, $response);
+ }
+
+ /**
+ * Connect to SMTP Server
+ *
+ * @return void
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ protected function _connect(): void
+ {
+ $this->_generateSocket();
+ if (!$this->_socket()->connect()) {
+ throw new SocketException('Unable to connect to SMTP server.');
+ }
+ $this->_smtpSend(null, '220');
+
+ $config = $this->_config;
+
+ $host = 'localhost';
+ if (isset($config['client'])) {
+ $host = $config['client'];
+ } else {
+ /** @var string $httpHost */
+ $httpHost = env('HTTP_HOST');
+ if ($httpHost) {
+ [$host] = explode(':', $httpHost);
+ }
+ }
+
+ try {
+ $this->_smtpSend("EHLO {$host}", '250');
+ if ($config['tls']) {
+ $this->_smtpSend('STARTTLS', '220');
+ $this->_socket()->enableCrypto('tls');
+ $this->_smtpSend("EHLO {$host}", '250');
+ }
+ } catch (SocketException $e) {
+ if ($config['tls']) {
+ throw new SocketException(
+ 'SMTP server did not accept the connection or trying to connect to non TLS SMTP server using TLS.',
+ null,
+ $e
+ );
+ }
+ try {
+ $this->_smtpSend("HELO {$host}", '250');
+ } catch (SocketException $e2) {
+ throw new SocketException('SMTP server did not accept the connection.', null, $e2);
+ }
+ }
+ }
+
+ /**
+ * Send authentication
+ *
+ * @return void
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ protected function _auth(): void
+ {
+ if (!isset($this->_config['username'], $this->_config['password'])) {
+ return;
+ }
+
+ $username = $this->_config['username'];
+ $password = $this->_config['password'];
+
+ $replyCode = $this->_authPlain($username, $password);
+ if ($replyCode === '235') {
+ return;
+ }
+
+ $this->_authLogin($username, $password);
+ }
+
+ /**
+ * Authenticate using AUTH PLAIN mechanism.
+ *
+ * @param string $username Username.
+ * @param string $password Password.
+ * @return string|null Response code for the command.
+ */
+ protected function _authPlain(string $username, string $password): ?string
+ {
+ return $this->_smtpSend(
+ sprintf(
+ 'AUTH PLAIN %s',
+ base64_encode(chr(0) . $username . chr(0) . $password)
+ ),
+ '235|504|534|535'
+ );
+ }
+
+ /**
+ * Authenticate using AUTH LOGIN mechanism.
+ *
+ * @param string $username Username.
+ * @param string $password Password.
+ * @return void
+ */
+ protected function _authLogin(string $username, string $password): void
+ {
+ $replyCode = $this->_smtpSend('AUTH LOGIN', '334|500|502|504');
+ if ($replyCode === '334') {
+ try {
+ $this->_smtpSend(base64_encode($username), '334');
+ } catch (SocketException $e) {
+ throw new SocketException('SMTP server did not accept the username.', null, $e);
+ }
+ try {
+ $this->_smtpSend(base64_encode($password), '235');
+ } catch (SocketException $e) {
+ throw new SocketException('SMTP server did not accept the password.', null, $e);
+ }
+ } elseif ($replyCode === '504') {
+ throw new SocketException('SMTP authentication method not allowed, check if SMTP server requires TLS.');
+ } else {
+ throw new SocketException(
+ 'AUTH command not recognized or not implemented, SMTP server may not require authentication.'
+ );
+ }
+ }
+
+ /**
+ * Prepares the `MAIL FROM` SMTP command.
+ *
+ * @param string $message The email address to send with the command.
+ * @return string
+ */
+ protected function _prepareFromCmd(string $message): string
+ {
+ return 'MAIL FROM:<' . $message . '>';
+ }
+
+ /**
+ * Prepares the `RCPT TO` SMTP command.
+ *
+ * @param string $message The email address to send with the command.
+ * @return string
+ */
+ protected function _prepareRcptCmd(string $message): string
+ {
+ return 'RCPT TO:<' . $message . '>';
+ }
+
+ /**
+ * Prepares the `from` email address.
+ *
+ * @param \Cake\Mailer\Message $message Message instance
+ * @return array
+ */
+ protected function _prepareFromAddress(Message $message): array
+ {
+ $from = $message->getReturnPath();
+ if (empty($from)) {
+ $from = $message->getFrom();
+ }
+
+ return $from;
+ }
+
+ /**
+ * Prepares the recipient email addresses.
+ *
+ * @param \Cake\Mailer\Message $message Message instance
+ * @return array
+ */
+ protected function _prepareRecipientAddresses(Message $message): array
+ {
+ $to = $message->getTo();
+ $cc = $message->getCc();
+ $bcc = $message->getBcc();
+
+ return array_merge(array_keys($to), array_keys($cc), array_keys($bcc));
+ }
+
+ /**
+ * Prepares the message body.
+ *
+ * @param \Cake\Mailer\Message $message Message instance
+ * @return string
+ */
+ protected function _prepareMessage(Message $message): string
+ {
+ $lines = $message->getBody();
+ $messages = [];
+ foreach ($lines as $line) {
+ if (!empty($line) && ($line[0] === '.')) {
+ $messages[] = '.' . $line;
+ } else {
+ $messages[] = $line;
+ }
+ }
+
+ return implode("\r\n", $messages);
+ }
+
+ /**
+ * Send emails
+ *
+ * @param \Cake\Mailer\Message $message Message message
+ * @throws \Cake\Network\Exception\SocketException
+ * @return void
+ */
+ protected function _sendRcpt(Message $message): void
+ {
+ $from = $this->_prepareFromAddress($message);
+ $this->_smtpSend($this->_prepareFromCmd(key($from)));
+
+ $messages = $this->_prepareRecipientAddresses($message);
+ foreach ($messages as $mail) {
+ $this->_smtpSend($this->_prepareRcptCmd($mail));
+ }
+ }
+
+ /**
+ * Send Data
+ *
+ * @param \Cake\Mailer\Message $message Message message
+ * @return void
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ protected function _sendData(Message $message): void
+ {
+ $this->_smtpSend('DATA', '354');
+
+ $headers = $message->getHeadersString([
+ 'from',
+ 'sender',
+ 'replyTo',
+ 'readReceipt',
+ 'to',
+ 'cc',
+ 'subject',
+ 'returnPath',
+ ]);
+ $message = $this->_prepareMessage($message);
+
+ $this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n.");
+ $this->_content = ['headers' => $headers, 'message' => $message];
+ }
+
+ /**
+ * Disconnect
+ *
+ * @return void
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ protected function _disconnect(): void
+ {
+ $this->_smtpSend('QUIT', false);
+ $this->_socket()->disconnect();
+ }
+
+ /**
+ * Helper method to generate socket
+ *
+ * @return void
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ protected function _generateSocket(): void
+ {
+ $this->_socket = new Socket($this->_config);
+ }
+
+ /**
+ * Protected method for sending data to SMTP connection
+ *
+ * @param string|null $data Data to be sent to SMTP server
+ * @param string|false $checkCode Code to check for in server response, false to skip
+ * @return string|null The matched code, or null if nothing matched
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ protected function _smtpSend(?string $data, $checkCode = '250'): ?string
+ {
+ $this->_lastResponse = [];
+
+ if ($data !== null) {
+ $this->_socket()->write($data . "\r\n");
+ }
+
+ $timeout = $this->_config['timeout'];
+
+ while ($checkCode !== false) {
+ $response = '';
+ $startTime = time();
+ while (substr($response, -2) !== "\r\n" && (time() - $startTime < $timeout)) {
+ $bytes = $this->_socket()->read();
+ if ($bytes === null) {
+ break;
+ }
+ $response .= $bytes;
+ }
+ // Catch empty or malformed responses.
+ if (substr($response, -2) !== "\r\n") {
+ // Use response message or assume operation timed out.
+ throw new SocketException($response ?: 'SMTP timeout.');
+ }
+ $responseLines = explode("\r\n", rtrim($response, "\r\n"));
+ $response = end($responseLines);
+
+ $this->_bufferResponseLines($responseLines);
+
+ if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) {
+ if ($code[2] === '-') {
+ continue;
+ }
+
+ return $code[1];
+ }
+ throw new SocketException(sprintf('SMTP Error: %s', $response));
+ }
+
+ return null;
+ }
+
+ /**
+ * Get socket instance.
+ *
+ * @return \Cake\Network\Socket
+ * @throws \RuntimeException If socket is not set.
+ */
+ protected function _socket(): Socket
+ {
+ if ($this->_socket === null) {
+ throw new \RuntimeException('Socket is null, but must be set.');
+ }
+
+ return $this->_socket;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/TransportFactory.php b/app/vendor/cakephp/cakephp/src/Mailer/TransportFactory.php
new file mode 100644
index 000000000..f90fc12c6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/TransportFactory.php
@@ -0,0 +1,117 @@
+
+ */
+ protected static $_dsnClassMap = [
+ 'debug' => Transport\DebugTransport::class,
+ 'mail' => Transport\MailTransport::class,
+ 'smtp' => Transport\SmtpTransport::class,
+ ];
+
+ /**
+ * Returns the Transport Registry used for creating and using transport instances.
+ *
+ * @return \Cake\Mailer\TransportRegistry
+ */
+ public static function getRegistry(): TransportRegistry
+ {
+ if (static::$_registry === null) {
+ static::$_registry = new TransportRegistry();
+ }
+
+ return static::$_registry;
+ }
+
+ /**
+ * Sets the Transport Registry instance used for creating and using transport instances.
+ *
+ * Also allows for injecting of a new registry instance.
+ *
+ * @param \Cake\Mailer\TransportRegistry $registry Injectable registry object.
+ * @return void
+ */
+ public static function setRegistry(TransportRegistry $registry): void
+ {
+ static::$_registry = $registry;
+ }
+
+ /**
+ * Finds and builds the instance of the required tranport class.
+ *
+ * @param string $name Name of the config array that needs a tranport instance built
+ * @return void
+ * @throws \InvalidArgumentException When a tranport cannot be created.
+ */
+ protected static function _buildTransport(string $name): void
+ {
+ if (!isset(static::$_config[$name])) {
+ throw new InvalidArgumentException(
+ sprintf('The "%s" transport configuration does not exist', $name)
+ );
+ }
+
+ if (is_array(static::$_config[$name]) && empty(static::$_config[$name]['className'])) {
+ throw new InvalidArgumentException(
+ sprintf('Transport config "%s" is invalid, the required `className` option is missing', $name)
+ );
+ }
+
+ static::getRegistry()->load($name, static::$_config[$name]);
+ }
+
+ /**
+ * Get transport instance.
+ *
+ * @param string $name Config name.
+ * @return \Cake\Mailer\AbstractTransport
+ */
+ public static function get(string $name): AbstractTransport
+ {
+ $registry = static::getRegistry();
+
+ if (isset($registry->{$name})) {
+ return $registry->{$name};
+ }
+
+ static::_buildTransport($name);
+
+ return $registry->{$name};
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Mailer/TransportRegistry.php b/app/vendor/cakephp/cakephp/src/Mailer/TransportRegistry.php
new file mode 100644
index 000000000..cba1594e9
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Mailer/TransportRegistry.php
@@ -0,0 +1,100 @@
+
+ */
+class TransportRegistry extends ObjectRegistry
+{
+ /**
+ * Resolve a mailer tranport classname.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class Partial classname to resolve or transport instance.
+ * @return string|null Either the correct classname or null.
+ * @psalm-return class-string|null
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ return App::className($class, 'Mailer/Transport', 'Transport');
+ }
+
+ /**
+ * Throws an exception when a cache engine is missing.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class The classname that is missing.
+ * @param string|null $plugin The plugin the cache is missing in.
+ * @return void
+ * @throws \BadMethodCallException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ throw new BadMethodCallException(sprintf('Mailer transport %s is not available.', $class));
+ }
+
+ /**
+ * Create the mailer transport instance.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string|\Cake\Mailer\AbstractTransport $class The classname or object to make.
+ * @param string $alias The alias of the object.
+ * @param array $config An array of settings to use for the cache engine.
+ * @return \Cake\Mailer\AbstractTransport The constructed transport class.
+ * @throws \RuntimeException when an object doesn't implement the correct interface.
+ */
+ protected function _create($class, string $alias, array $config): AbstractTransport
+ {
+ if (is_object($class)) {
+ $instance = $class;
+ } else {
+ $instance = new $class($config);
+ }
+
+ if ($instance instanceof AbstractTransport) {
+ return $instance;
+ }
+
+ throw new RuntimeException(
+ 'Mailer transports must use Cake\Mailer\AbstractTransport as a base class.'
+ );
+ }
+
+ /**
+ * Remove a single adapter from the registry.
+ *
+ * @param string $name The adapter name.
+ * @return $this
+ */
+ public function unload(string $name)
+ {
+ unset($this->_loaded[$name]);
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Network/Exception/SocketException.php b/app/vendor/cakephp/cakephp/src/Network/Exception/SocketException.php
new file mode 100644
index 000000000..dadfb4997
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Network/Exception/SocketException.php
@@ -0,0 +1,25 @@
+ false,
+ 'host' => 'localhost',
+ 'protocol' => 'tcp',
+ 'port' => 80,
+ 'timeout' => 30,
+ ];
+
+ /**
+ * Reference to socket connection resource
+ *
+ * @var resource|null
+ */
+ public $connection;
+
+ /**
+ * This boolean contains the current state of the Socket class
+ *
+ * @var bool
+ */
+ public $connected = false;
+
+ /**
+ * This variable contains an array with the last error number (num) and string (str)
+ *
+ * @var array
+ */
+ public $lastError = [];
+
+ /**
+ * True if the socket stream is encrypted after a Cake\Network\Socket::enableCrypto() call
+ *
+ * @var bool
+ */
+ public $encrypted = false;
+
+ /**
+ * Contains all the encryption methods available
+ *
+ * @var array
+ */
+ protected $_encryptMethods = [
+ // phpcs:disable
+ 'sslv23_client' => STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
+ 'tls_client' => STREAM_CRYPTO_METHOD_TLS_CLIENT,
+ 'tlsv10_client' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT,
+ 'tlsv11_client' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT,
+ 'tlsv12_client' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
+ 'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER,
+ 'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER,
+ 'tlsv10_server' => STREAM_CRYPTO_METHOD_TLSv1_0_SERVER,
+ 'tlsv11_server' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER,
+ 'tlsv12_server' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
+ // phpcs:enable
+ ];
+
+ /**
+ * Used to capture connection warnings which can happen when there are
+ * SSL errors for example.
+ *
+ * @var array
+ */
+ protected $_connectionErrors = [];
+
+ /**
+ * Constructor.
+ *
+ * @param array $config Socket configuration, which will be merged with the base configuration
+ * @see \Cake\Network\Socket::$_baseConfig
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+ }
+
+ /**
+ * Connect the socket to the given host and port.
+ *
+ * @return bool Success
+ * @throws \Cake\Network\Exception\SocketException
+ */
+ public function connect(): bool
+ {
+ if ($this->connection) {
+ $this->disconnect();
+ }
+
+ $hasProtocol = strpos($this->_config['host'], '://') !== false;
+ if ($hasProtocol) {
+ [$this->_config['protocol'], $this->_config['host']] = explode('://', $this->_config['host']);
+ }
+ $scheme = null;
+ if (!empty($this->_config['protocol'])) {
+ $scheme = $this->_config['protocol'] . '://';
+ }
+
+ $this->_setSslContext($this->_config['host']);
+ if (!empty($this->_config['context'])) {
+ $context = stream_context_create($this->_config['context']);
+ } else {
+ $context = stream_context_create();
+ }
+
+ $connectAs = STREAM_CLIENT_CONNECT;
+ if ($this->_config['persistent']) {
+ $connectAs |= STREAM_CLIENT_PERSISTENT;
+ }
+
+ /** @psalm-suppress InvalidArgument */
+ set_error_handler([$this, '_connectionErrorHandler']);
+ $remoteSocketTarget = $scheme . $this->_config['host'];
+ $port = (int)$this->_config['port'];
+ if ($port > 0) {
+ $remoteSocketTarget .= ':' . $port;
+ }
+
+ $errNum = 0;
+ $errStr = '';
+ $this->connection = $this->_getStreamSocketClient(
+ $remoteSocketTarget,
+ $errNum,
+ $errStr,
+ (int)$this->_config['timeout'],
+ $connectAs,
+ $context
+ );
+ restore_error_handler();
+
+ if ($this->connection === null && (!$errNum || !$errStr)) {
+ $this->setLastError($errNum, $errStr);
+ throw new SocketException($errStr, $errNum);
+ }
+
+ if ($this->connection === null && $this->_connectionErrors) {
+ $message = implode("\n", $this->_connectionErrors);
+ throw new SocketException($message, E_WARNING);
+ }
+
+ $this->connected = is_resource($this->connection);
+ if ($this->connected) {
+ /** @psalm-suppress PossiblyNullArgument */
+ stream_set_timeout($this->connection, (int)$this->_config['timeout']);
+ }
+
+ return $this->connected;
+ }
+
+ /**
+ * Create a stream socket client. Mock utility.
+ *
+ * @param string $remoteSocketTarget remote socket
+ * @param int $errNum error number
+ * @param string $errStr error string
+ * @param int $timeout timeout
+ * @param int $connectAs flags
+ * @param resource $context context
+ * @return resource|null
+ */
+ protected function _getStreamSocketClient($remoteSocketTarget, &$errNum, &$errStr, $timeout, $connectAs, $context)
+ {
+ $resource = stream_socket_client(
+ $remoteSocketTarget,
+ $errNum,
+ $errStr,
+ $timeout,
+ $connectAs,
+ $context
+ );
+
+ if ($resource) {
+ return $resource;
+ }
+
+ return null;
+ }
+
+ /**
+ * Configure the SSL context options.
+ *
+ * @param string $host The host name being connected to.
+ * @return void
+ */
+ protected function _setSslContext(string $host): void
+ {
+ foreach ($this->_config as $key => $value) {
+ if (substr($key, 0, 4) !== 'ssl_') {
+ continue;
+ }
+ $contextKey = substr($key, 4);
+ if (empty($this->_config['context']['ssl'][$contextKey])) {
+ $this->_config['context']['ssl'][$contextKey] = $value;
+ }
+ unset($this->_config[$key]);
+ }
+ if (!isset($this->_config['context']['ssl']['SNI_enabled'])) {
+ $this->_config['context']['ssl']['SNI_enabled'] = true;
+ }
+ if (empty($this->_config['context']['ssl']['peer_name'])) {
+ $this->_config['context']['ssl']['peer_name'] = $host;
+ }
+ if (empty($this->_config['context']['ssl']['cafile'])) {
+ $this->_config['context']['ssl']['cafile'] = CaBundle::getBundledCaBundlePath();
+ }
+ if (!empty($this->_config['context']['ssl']['verify_host'])) {
+ $this->_config['context']['ssl']['CN_match'] = $host;
+ }
+ unset($this->_config['context']['ssl']['verify_host']);
+ }
+
+ /**
+ * socket_stream_client() does not populate errNum, or $errStr when there are
+ * connection errors, as in the case of SSL verification failure.
+ *
+ * Instead we need to handle those errors manually.
+ *
+ * @param int $code Code number.
+ * @param string $message Message.
+ * @return void
+ */
+ protected function _connectionErrorHandler(int $code, string $message): void
+ {
+ $this->_connectionErrors[] = $message;
+ }
+
+ /**
+ * Get the connection context.
+ *
+ * @return array|null Null when there is no connection, an array when there is.
+ */
+ public function context(): ?array
+ {
+ if (!$this->connection) {
+ return null;
+ }
+
+ return stream_context_get_options($this->connection);
+ }
+
+ /**
+ * Get the host name of the current connection.
+ *
+ * @return string Host name
+ */
+ public function host(): string
+ {
+ if (Validation::ip($this->_config['host'])) {
+ return gethostbyaddr($this->_config['host']);
+ }
+
+ return gethostbyaddr($this->address());
+ }
+
+ /**
+ * Get the IP address of the current connection.
+ *
+ * @return string IP address
+ */
+ public function address(): string
+ {
+ if (Validation::ip($this->_config['host'])) {
+ return $this->_config['host'];
+ }
+
+ return gethostbyname($this->_config['host']);
+ }
+
+ /**
+ * Get all IP addresses associated with the current connection.
+ *
+ * @return array IP addresses
+ */
+ public function addresses(): array
+ {
+ if (Validation::ip($this->_config['host'])) {
+ return [$this->_config['host']];
+ }
+
+ return gethostbynamel($this->_config['host']);
+ }
+
+ /**
+ * Get the last error as a string.
+ *
+ * @return string|null Last error
+ */
+ public function lastError(): ?string
+ {
+ if (!empty($this->lastError)) {
+ return $this->lastError['num'] . ': ' . $this->lastError['str'];
+ }
+
+ return null;
+ }
+
+ /**
+ * Set the last error.
+ *
+ * @param int|null $errNum Error code
+ * @param string $errStr Error string
+ * @return void
+ */
+ public function setLastError(?int $errNum, string $errStr): void
+ {
+ $this->lastError = ['num' => $errNum, 'str' => $errStr];
+ }
+
+ /**
+ * Write data to the socket.
+ *
+ * @param string $data The data to write to the socket.
+ * @return int Bytes written.
+ */
+ public function write(string $data): int
+ {
+ if (!$this->connected && !$this->connect()) {
+ return 0;
+ }
+ $totalBytes = strlen($data);
+ $written = 0;
+ while ($written < $totalBytes) {
+ /** @psalm-suppress PossiblyNullArgument */
+ $rv = fwrite($this->connection, substr($data, $written));
+ if ($rv === false || $rv === 0) {
+ return $written;
+ }
+ $written += $rv;
+ }
+
+ return $written;
+ }
+
+ /**
+ * Read data from the socket. Returns null if no data is available or no connection could be
+ * established.
+ *
+ * @param int $length Optional buffer length to read; defaults to 1024
+ * @return string|null Socket data
+ */
+ public function read(int $length = 1024): ?string
+ {
+ if (!$this->connected && !$this->connect()) {
+ return null;
+ }
+
+ /** @psalm-suppress PossiblyNullArgument */
+ if (!feof($this->connection)) {
+ $buffer = fread($this->connection, $length);
+ $info = stream_get_meta_data($this->connection);
+ if ($info['timed_out']) {
+ $this->setLastError(E_WARNING, 'Connection timed out');
+
+ return null;
+ }
+
+ return $buffer;
+ }
+
+ return null;
+ }
+
+ /**
+ * Disconnect the socket from the current connection.
+ *
+ * @return bool Success
+ */
+ public function disconnect(): bool
+ {
+ if (!is_resource($this->connection)) {
+ $this->connected = false;
+
+ return true;
+ }
+ /** @psalm-suppress InvalidPropertyAssignmentValue */
+ $this->connected = !fclose($this->connection);
+
+ if (!$this->connected) {
+ $this->connection = null;
+ }
+
+ return !$this->connected;
+ }
+
+ /**
+ * Destructor, used to disconnect from current connection.
+ */
+ public function __destruct()
+ {
+ $this->disconnect();
+ }
+
+ /**
+ * Resets the state of this Socket instance to it's initial state (before Object::__construct got executed)
+ *
+ * @param array|null $state Array with key and values to reset
+ * @return void
+ */
+ public function reset(?array $state = null): void
+ {
+ if (empty($state)) {
+ static $initalState = [];
+ if (empty($initalState)) {
+ $initalState = get_class_vars(self::class);
+ }
+ $state = $initalState;
+ }
+
+ foreach ($state as $property => $value) {
+ $this->{$property} = $value;
+ }
+ }
+
+ /**
+ * Encrypts current stream socket, using one of the defined encryption methods
+ *
+ * @param string $type can be one of 'ssl2', 'ssl3', 'ssl23' or 'tls'
+ * @param string $clientOrServer can be one of 'client', 'server'. Default is 'client'
+ * @param bool $enable enable or disable encryption. Default is true (enable)
+ * @return void
+ * @throws \InvalidArgumentException When an invalid encryption scheme is chosen.
+ * @throws \Cake\Network\Exception\SocketException When attempting to enable SSL/TLS fails
+ * @see stream_socket_enable_crypto
+ */
+ public function enableCrypto(string $type, string $clientOrServer = 'client', bool $enable = true): void
+ {
+ if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) {
+ throw new InvalidArgumentException('Invalid encryption scheme chosen');
+ }
+ $method = $this->_encryptMethods[$type . '_' . $clientOrServer];
+
+ if ($method === STREAM_CRYPTO_METHOD_TLS_CLIENT) {
+ $method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
+ }
+ if ($method === STREAM_CRYPTO_METHOD_TLS_SERVER) {
+ $method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
+ }
+
+ try {
+ if ($this->connection === null) {
+ throw new CakeException('You must call connect() first.');
+ }
+ $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, $method);
+ } catch (Exception $e) {
+ $this->setLastError(null, $e->getMessage());
+ throw new SocketException($e->getMessage(), null, $e);
+ }
+
+ if ($enableCryptoResult === true) {
+ $this->encrypted = $enable;
+
+ return;
+ }
+
+ $errorMessage = 'Unable to perform enableCrypto operation on the current socket';
+ $this->setLastError(null, $errorMessage);
+ throw new SocketException($errorMessage);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association.php b/app/vendor/cakephp/cakephp/src/ORM/Association.php
new file mode 100644
index 000000000..218cade86
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association.php
@@ -0,0 +1,1258 @@
+{'_' . $property} = $options[$property];
+ }
+ }
+
+ if (empty($this->_className)) {
+ $this->_className = $alias;
+ }
+
+ [, $name] = pluginSplit($alias);
+ $this->_name = $name;
+
+ $this->_options($options);
+
+ if (!empty($options['strategy'])) {
+ $this->setStrategy($options['strategy']);
+ }
+ }
+
+ /**
+ * Sets the name for this association, usually the alias
+ * assigned to the target associated table
+ *
+ * @param string $name Name to be assigned
+ * @return $this
+ */
+ public function setName(string $name)
+ {
+ if ($this->_targetTable !== null) {
+ $alias = $this->_targetTable->getAlias();
+ if ($alias !== $name) {
+ throw new InvalidArgumentException(sprintf(
+ 'Association name "%s" does not match target table alias "%s".',
+ $name,
+ $alias
+ ));
+ }
+ }
+
+ $this->_name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Gets the name for this association, usually the alias
+ * assigned to the target associated table
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->_name;
+ }
+
+ /**
+ * Sets whether or not cascaded deletes should also fire callbacks.
+ *
+ * @param bool $cascadeCallbacks cascade callbacks switch value
+ * @return $this
+ */
+ public function setCascadeCallbacks(bool $cascadeCallbacks)
+ {
+ $this->_cascadeCallbacks = $cascadeCallbacks;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether or not cascaded deletes should also fire callbacks.
+ *
+ * @return bool
+ */
+ public function getCascadeCallbacks(): bool
+ {
+ return $this->_cascadeCallbacks;
+ }
+
+ /**
+ * Sets the class name of the target table object.
+ *
+ * @param string $className Class name to set.
+ * @return $this
+ * @throws \InvalidArgumentException In case the class name is set after the target table has been
+ * resolved, and it doesn't match the target table's class name.
+ */
+ public function setClassName(string $className)
+ {
+ if (
+ $this->_targetTable !== null &&
+ get_class($this->_targetTable) !== App::className($className, 'Model/Table', 'Table')
+ ) {
+ throw new InvalidArgumentException(sprintf(
+ 'The class name "%s" doesn\'t match the target table class name of "%s".',
+ $className,
+ get_class($this->_targetTable)
+ ));
+ }
+
+ $this->_className = $className;
+
+ return $this;
+ }
+
+ /**
+ * Gets the class name of the target table object.
+ *
+ * @return string
+ */
+ public function getClassName(): string
+ {
+ return $this->_className;
+ }
+
+ /**
+ * Sets the table instance for the source side of the association.
+ *
+ * @param \Cake\ORM\Table $table the instance to be assigned as source side
+ * @return $this
+ */
+ public function setSource(Table $table)
+ {
+ $this->_sourceTable = $table;
+
+ return $this;
+ }
+
+ /**
+ * Gets the table instance for the source side of the association.
+ *
+ * @return \Cake\ORM\Table
+ */
+ public function getSource(): Table
+ {
+ return $this->_sourceTable;
+ }
+
+ /**
+ * Sets the table instance for the target side of the association.
+ *
+ * @param \Cake\ORM\Table $table the instance to be assigned as target side
+ * @return $this
+ */
+ public function setTarget(Table $table)
+ {
+ $this->_targetTable = $table;
+
+ return $this;
+ }
+
+ /**
+ * Gets the table instance for the target side of the association.
+ *
+ * @return \Cake\ORM\Table
+ */
+ public function getTarget(): Table
+ {
+ if ($this->_targetTable === null) {
+ if (strpos($this->_className, '.')) {
+ [$plugin] = pluginSplit($this->_className, true);
+ $registryAlias = (string)$plugin . $this->_name;
+ } else {
+ $registryAlias = $this->_name;
+ }
+
+ $tableLocator = $this->getTableLocator();
+
+ $config = [];
+ $exists = $tableLocator->exists($registryAlias);
+ if (!$exists) {
+ $config = ['className' => $this->_className];
+ }
+ $this->_targetTable = $tableLocator->get($registryAlias, $config);
+
+ if ($exists) {
+ $className = App::className($this->_className, 'Model/Table', 'Table') ?: Table::class;
+
+ if (!$this->_targetTable instanceof $className) {
+ $errorMessage = '%s association "%s" of type "%s" to "%s" doesn\'t match the expected class "%s". ';
+ $errorMessage .= 'You can\'t have an association of the same name with a different target ';
+ $errorMessage .= '"className" option anywhere in your app.';
+
+ throw new RuntimeException(sprintf(
+ $errorMessage,
+ $this->_sourceTable === null ? 'null' : get_class($this->_sourceTable),
+ $this->getName(),
+ $this->type(),
+ get_class($this->_targetTable),
+ $className
+ ));
+ }
+ }
+ }
+
+ return $this->_targetTable;
+ }
+
+ /**
+ * Sets a list of conditions to be always included when fetching records from
+ * the target association.
+ *
+ * @param array|\Closure $conditions list of conditions to be used
+ * @see \Cake\Database\Query::where() for examples on the format of the array
+ * @return \Cake\ORM\Association
+ */
+ public function setConditions($conditions)
+ {
+ $this->_conditions = $conditions;
+
+ return $this;
+ }
+
+ /**
+ * Gets a list of conditions to be always included when fetching records from
+ * the target association.
+ *
+ * @see \Cake\Database\Query::where() for examples on the format of the array
+ * @return array|\Closure
+ */
+ public function getConditions()
+ {
+ return $this->_conditions;
+ }
+
+ /**
+ * Sets the name of the field representing the binding field with the target table.
+ * When not manually specified the primary key of the owning side table is used.
+ *
+ * @param string|string[] $key the table field or fields to be used to link both tables together
+ * @return $this
+ */
+ public function setBindingKey($key)
+ {
+ $this->_bindingKey = $key;
+
+ return $this;
+ }
+
+ /**
+ * Gets the name of the field representing the binding field with the target table.
+ * When not manually specified the primary key of the owning side table is used.
+ *
+ * @return string|string[]
+ */
+ public function getBindingKey()
+ {
+ if ($this->_bindingKey === null) {
+ $this->_bindingKey = $this->isOwningSide($this->getSource()) ?
+ $this->getSource()->getPrimaryKey() :
+ $this->getTarget()->getPrimaryKey();
+ }
+
+ return $this->_bindingKey;
+ }
+
+ /**
+ * Gets the name of the field representing the foreign key to the target table.
+ *
+ * @return string|string[]
+ */
+ public function getForeignKey()
+ {
+ return $this->_foreignKey;
+ }
+
+ /**
+ * Sets the name of the field representing the foreign key to the target table.
+ *
+ * @param string|string[] $key the key or keys to be used to link both tables together
+ * @return $this
+ */
+ public function setForeignKey($key)
+ {
+ $this->_foreignKey = $key;
+
+ return $this;
+ }
+
+ /**
+ * Sets whether the records on the target table are dependent on the source table.
+ *
+ * This is primarily used to indicate that records should be removed if the owning record in
+ * the source table is deleted.
+ *
+ * If no parameters are passed the current setting is returned.
+ *
+ * @param bool $dependent Set the dependent mode. Use null to read the current state.
+ * @return $this
+ */
+ public function setDependent(bool $dependent)
+ {
+ $this->_dependent = $dependent;
+
+ return $this;
+ }
+
+ /**
+ * Sets whether the records on the target table are dependent on the source table.
+ *
+ * This is primarily used to indicate that records should be removed if the owning record in
+ * the source table is deleted.
+ *
+ * @return bool
+ */
+ public function getDependent(): bool
+ {
+ return $this->_dependent;
+ }
+
+ /**
+ * Whether this association can be expressed directly in a query join
+ *
+ * @param array $options custom options key that could alter the return value
+ * @return bool
+ */
+ public function canBeJoined(array $options = []): bool
+ {
+ $strategy = $options['strategy'] ?? $this->getStrategy();
+
+ return $strategy === $this::STRATEGY_JOIN;
+ }
+
+ /**
+ * Sets the type of join to be used when adding the association to a query.
+ *
+ * @param string $type the join type to be used (e.g. INNER)
+ * @return $this
+ */
+ public function setJoinType(string $type)
+ {
+ $this->_joinType = $type;
+
+ return $this;
+ }
+
+ /**
+ * Gets the type of join to be used when adding the association to a query.
+ *
+ * @return string
+ */
+ public function getJoinType(): string
+ {
+ return $this->_joinType;
+ }
+
+ /**
+ * Sets the property name that should be filled with data from the target table
+ * in the source table record.
+ *
+ * @param string $name The name of the association property. Use null to read the current value.
+ * @return $this
+ */
+ public function setProperty(string $name)
+ {
+ $this->_propertyName = $name;
+
+ return $this;
+ }
+
+ /**
+ * Gets the property name that should be filled with data from the target table
+ * in the source table record.
+ *
+ * @return string
+ */
+ public function getProperty(): string
+ {
+ if (!$this->_propertyName) {
+ $this->_propertyName = $this->_propertyName();
+ if (in_array($this->_propertyName, $this->_sourceTable->getSchema()->columns(), true)) {
+ $msg = 'Association property name "%s" clashes with field of same name of table "%s".' .
+ ' You should explicitly specify the "propertyName" option.';
+ trigger_error(
+ sprintf($msg, $this->_propertyName, $this->_sourceTable->getTable()),
+ E_USER_WARNING
+ );
+ }
+ }
+
+ return $this->_propertyName;
+ }
+
+ /**
+ * Returns default property name based on association name.
+ *
+ * @return string
+ */
+ protected function _propertyName(): string
+ {
+ [, $name] = pluginSplit($this->_name);
+
+ return Inflector::underscore($name);
+ }
+
+ /**
+ * Sets the strategy name to be used to fetch associated records. Keep in mind
+ * that some association types might not implement but a default strategy,
+ * rendering any changes to this setting void.
+ *
+ * @param string $name The strategy type. Use null to read the current value.
+ * @return $this
+ * @throws \InvalidArgumentException When an invalid strategy is provided.
+ */
+ public function setStrategy(string $name)
+ {
+ if (!in_array($name, $this->_validStrategies, true)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Invalid strategy "%s" was provided. Valid options are (%s).',
+ $name,
+ implode(', ', $this->_validStrategies)
+ ));
+ }
+ $this->_strategy = $name;
+
+ return $this;
+ }
+
+ /**
+ * Gets the strategy name to be used to fetch associated records. Keep in mind
+ * that some association types might not implement but a default strategy,
+ * rendering any changes to this setting void.
+ *
+ * @return string
+ */
+ public function getStrategy(): string
+ {
+ return $this->_strategy;
+ }
+
+ /**
+ * Gets the default finder to use for fetching rows from the target table.
+ *
+ * @return string|array
+ */
+ public function getFinder()
+ {
+ return $this->_finder;
+ }
+
+ /**
+ * Sets the default finder to use for fetching rows from the target table.
+ *
+ * @param string|array $finder the finder name to use or array of finder name and option.
+ * @return $this
+ */
+ public function setFinder($finder)
+ {
+ $this->_finder = $finder;
+
+ return $this;
+ }
+
+ /**
+ * Override this function to initialize any concrete association class, it will
+ * get passed the original list of options used in the constructor
+ *
+ * @param array $options List of options used for initialization
+ * @return void
+ */
+ protected function _options(array $options): void
+ {
+ }
+
+ /**
+ * Alters a Query object to include the associated target table data in the final
+ * result
+ *
+ * The options array accept the following keys:
+ *
+ * - includeFields: Whether to include target model fields in the result or not
+ * - foreignKey: The name of the field to use as foreign key, if false none
+ * will be used
+ * - conditions: array with a list of conditions to filter the join with, this
+ * will be merged with any conditions originally configured for this association
+ * - fields: a list of fields in the target table to include in the result
+ * - type: The type of join to be used (e.g. INNER)
+ * the records found on this association
+ * - aliasPath: A dot separated string representing the path of association names
+ * followed from the passed query main table to this association.
+ * - propertyPath: A dot separated string representing the path of association
+ * properties to be followed from the passed query main entity to this
+ * association
+ * - joinType: The SQL join type to use in the query.
+ * - negateMatch: Will append a condition to the passed query for excluding matches.
+ * with this association.
+ *
+ * @param \Cake\ORM\Query $query the query to be altered to include the target table data
+ * @param array $options Any extra options or overrides to be taken in account
+ * @return void
+ * @throws \RuntimeException if the query builder passed does not return a query
+ * object
+ */
+ public function attachTo(Query $query, array $options = []): void
+ {
+ $target = $this->getTarget();
+ $joinType = empty($options['joinType']) ? $this->getJoinType() : $options['joinType'];
+ $table = $target->getTable();
+
+ $options += [
+ 'includeFields' => true,
+ 'foreignKey' => $this->getForeignKey(),
+ 'conditions' => [],
+ 'fields' => [],
+ 'type' => $joinType,
+ 'table' => $table,
+ 'finder' => $this->getFinder(),
+ ];
+
+ if (!empty($options['foreignKey'])) {
+ $joinCondition = $this->_joinCondition($options);
+ if ($joinCondition) {
+ $options['conditions'][] = $joinCondition;
+ }
+ }
+
+ [$finder, $opts] = $this->_extractFinder($options['finder']);
+ $dummy = $this
+ ->find($finder, $opts)
+ ->eagerLoaded(true);
+
+ if (!empty($options['queryBuilder'])) {
+ $dummy = $options['queryBuilder']($dummy);
+ if (!($dummy instanceof Query)) {
+ throw new RuntimeException(sprintf(
+ 'Query builder for association "%s" did not return a query',
+ $this->getName()
+ ));
+ }
+ }
+
+ if (
+ !empty($options['matching']) &&
+ $this->_strategy === static::STRATEGY_JOIN &&
+ $dummy->getContain()
+ ) {
+ throw new RuntimeException(
+ "`{$this->getName()}` association cannot contain() associations when using JOIN strategy."
+ );
+ }
+
+ $dummy->where($options['conditions']);
+ $this->_dispatchBeforeFind($dummy);
+
+ $joinOptions = ['table' => 1, 'conditions' => 1, 'type' => 1];
+ $options['conditions'] = $dummy->clause('where');
+ $query->join([$this->_name => array_intersect_key($options, $joinOptions)]);
+
+ $this->_appendFields($query, $dummy, $options);
+ $this->_formatAssociationResults($query, $dummy, $options);
+ $this->_bindNewAssociations($query, $dummy, $options);
+ $this->_appendNotMatching($query, $options);
+ }
+
+ /**
+ * Conditionally adds a condition to the passed Query that will make it find
+ * records where there is no match with this association.
+ *
+ * @param \Cake\ORM\Query $query The query to modify
+ * @param array $options Options array containing the `negateMatch` key.
+ * @return void
+ */
+ protected function _appendNotMatching(Query $query, array $options): void
+ {
+ $target = $this->_targetTable;
+ if (!empty($options['negateMatch'])) {
+ $primaryKey = $query->aliasFields((array)$target->getPrimaryKey(), $this->_name);
+ $query->andWhere(function ($exp) use ($primaryKey) {
+ array_map([$exp, 'isNull'], $primaryKey);
+
+ return $exp;
+ });
+ }
+ }
+
+ /**
+ * Correctly nests a result row associated values into the correct array keys inside the
+ * source results.
+ *
+ * @param array $row The row to transform
+ * @param string $nestKey The array key under which the results for this association
+ * should be found
+ * @param bool $joined Whether or not the row is a result of a direct join
+ * with this association
+ * @param string|null $targetProperty The property name in the source results where the association
+ * data shuld be nested in. Will use the default one if not provided.
+ * @return array
+ */
+ public function transformRow(array $row, string $nestKey, bool $joined, ?string $targetProperty = null): array
+ {
+ $sourceAlias = $this->getSource()->getAlias();
+ $nestKey = $nestKey ?: $this->_name;
+ $targetProperty = $targetProperty ?: $this->getProperty();
+ if (isset($row[$sourceAlias])) {
+ $row[$sourceAlias][$targetProperty] = $row[$nestKey];
+ unset($row[$nestKey]);
+ }
+
+ return $row;
+ }
+
+ /**
+ * Returns a modified row after appending a property for this association
+ * with the default empty value according to whether the association was
+ * joined or fetched externally.
+ *
+ * @param array $row The row to set a default on.
+ * @param bool $joined Whether or not the row is a result of a direct join
+ * with this association
+ * @return array
+ */
+ public function defaultRowValue(array $row, bool $joined): array
+ {
+ $sourceAlias = $this->getSource()->getAlias();
+ if (isset($row[$sourceAlias])) {
+ $row[$sourceAlias][$this->getProperty()] = null;
+ }
+
+ return $row;
+ }
+
+ /**
+ * Proxies the finding operation to the target table's find method
+ * and modifies the query accordingly based of this association
+ * configuration
+ *
+ * @param string|array|null $type the type of query to perform, if an array is passed,
+ * it will be interpreted as the `$options` parameter
+ * @param array $options The options to for the find
+ * @see \Cake\ORM\Table::find()
+ * @return \Cake\ORM\Query
+ */
+ public function find($type = null, array $options = []): Query
+ {
+ $type = $type ?: $this->getFinder();
+ [$type, $opts] = $this->_extractFinder($type);
+
+ return $this->getTarget()
+ ->find($type, $options + $opts)
+ ->where($this->getConditions());
+ }
+
+ /**
+ * Proxies the operation to the target table's exists method after
+ * appending the default conditions for this association
+ *
+ * @param array|\Closure|\Cake\Database\ExpressionInterface $conditions The conditions to use
+ * for checking if any record matches.
+ * @see \Cake\ORM\Table::exists()
+ * @return bool
+ */
+ public function exists($conditions): bool
+ {
+ $conditions = $this->find()
+ ->where($conditions)
+ ->clause('where');
+
+ return $this->getTarget()->exists($conditions);
+ }
+
+ /**
+ * Proxies the update operation to the target table's updateAll method
+ *
+ * @param array $fields A hash of field => new value.
+ * @param mixed $conditions Conditions to be used, accepts anything Query::where()
+ * can take.
+ * @see \Cake\ORM\Table::updateAll()
+ * @return int Count Returns the affected rows.
+ */
+ public function updateAll(array $fields, $conditions): int
+ {
+ $expression = $this->find()
+ ->where($conditions)
+ ->clause('where');
+
+ return $this->getTarget()->updateAll($fields, $expression);
+ }
+
+ /**
+ * Proxies the delete operation to the target table's deleteAll method
+ *
+ * @param mixed $conditions Conditions to be used, accepts anything Query::where()
+ * can take.
+ * @return int Returns the number of affected rows.
+ * @see \Cake\ORM\Table::deleteAll()
+ */
+ public function deleteAll($conditions): int
+ {
+ $expression = $this->find()
+ ->where($conditions)
+ ->clause('where');
+
+ return $this->getTarget()->deleteAll($expression);
+ }
+
+ /**
+ * Returns true if the eager loading process will require a set of the owning table's
+ * binding keys in order to use them as a filter in the finder query.
+ *
+ * @param array $options The options containing the strategy to be used.
+ * @return bool true if a list of keys will be required
+ */
+ public function requiresKeys(array $options = []): bool
+ {
+ $strategy = $options['strategy'] ?? $this->getStrategy();
+
+ return $strategy === static::STRATEGY_SELECT;
+ }
+
+ /**
+ * Triggers beforeFind on the target table for the query this association is
+ * attaching to
+ *
+ * @param \Cake\ORM\Query $query the query this association is attaching itself to
+ * @return void
+ */
+ protected function _dispatchBeforeFind(Query $query): void
+ {
+ $query->triggerBeforeFind();
+ }
+
+ /**
+ * Helper function used to conditionally append fields to the select clause of
+ * a query from the fields found in another query object.
+ *
+ * @param \Cake\ORM\Query $query the query that will get the fields appended to
+ * @param \Cake\ORM\Query $surrogate the query having the fields to be copied from
+ * @param array $options options passed to the method `attachTo`
+ * @return void
+ */
+ protected function _appendFields(Query $query, Query $surrogate, array $options): void
+ {
+ if ($query->getEagerLoader()->isAutoFieldsEnabled() === false) {
+ return;
+ }
+
+ $fields = $surrogate->clause('select') ?: $options['fields'];
+ $target = $this->_targetTable;
+ $autoFields = $surrogate->isAutoFieldsEnabled();
+
+ if (empty($fields) && !$autoFields) {
+ if ($options['includeFields'] && ($fields === null || $fields !== false)) {
+ $fields = $target->getSchema()->columns();
+ }
+ }
+
+ if ($autoFields === true) {
+ $fields = array_filter((array)$fields);
+ $fields = array_merge($fields, $target->getSchema()->columns());
+ }
+
+ if ($fields) {
+ $query->select($query->aliasFields($fields, $this->_name));
+ }
+ $query->addDefaultTypes($target);
+ }
+
+ /**
+ * Adds a formatter function to the passed `$query` if the `$surrogate` query
+ * declares any other formatter. Since the `$surrogate` query correspond to
+ * the associated target table, the resulting formatter will be the result of
+ * applying the surrogate formatters to only the property corresponding to
+ * such table.
+ *
+ * @param \Cake\ORM\Query $query the query that will get the formatter applied to
+ * @param \Cake\ORM\Query $surrogate the query having formatters for the associated
+ * target table.
+ * @param array $options options passed to the method `attachTo`
+ * @return void
+ */
+ protected function _formatAssociationResults(Query $query, Query $surrogate, array $options): void
+ {
+ $formatters = $surrogate->getResultFormatters();
+
+ if (!$formatters || empty($options['propertyPath'])) {
+ return;
+ }
+
+ $property = $options['propertyPath'];
+ $propertyPath = explode('.', $property);
+ $query->formatResults(function ($results, $query) use ($formatters, $property, $propertyPath) {
+ $extracted = [];
+ foreach ($results as $result) {
+ foreach ($propertyPath as $propertyPathItem) {
+ if (!isset($result[$propertyPathItem])) {
+ $result = null;
+ break;
+ }
+ $result = $result[$propertyPathItem];
+ }
+ $extracted[] = $result;
+ }
+ $extracted = new Collection($extracted);
+ foreach ($formatters as $callable) {
+ $extracted = new ResultSetDecorator($callable($extracted, $query));
+ }
+
+ /** @var \Cake\Collection\CollectionInterface $results */
+ $results = $results->insert($property, $extracted);
+ if ($query->isHydrationEnabled()) {
+ $results = $results->map(function ($result) {
+ $result->clean();
+
+ return $result;
+ });
+ }
+
+ return $results;
+ }, Query::PREPEND);
+ }
+
+ /**
+ * Applies all attachable associations to `$query` out of the containments found
+ * in the `$surrogate` query.
+ *
+ * Copies all contained associations from the `$surrogate` query into the
+ * passed `$query`. Containments are altered so that they respect the associations
+ * chain from which they originated.
+ *
+ * @param \Cake\ORM\Query $query the query that will get the associations attached to
+ * @param \Cake\ORM\Query $surrogate the query having the containments to be attached
+ * @param array $options options passed to the method `attachTo`
+ * @return void
+ */
+ protected function _bindNewAssociations(Query $query, Query $surrogate, array $options): void
+ {
+ $loader = $surrogate->getEagerLoader();
+ $contain = $loader->getContain();
+ $matching = $loader->getMatching();
+
+ if (!$contain && !$matching) {
+ return;
+ }
+
+ $newContain = [];
+ foreach ($contain as $alias => $value) {
+ $newContain[$options['aliasPath'] . '.' . $alias] = $value;
+ }
+
+ $eagerLoader = $query->getEagerLoader();
+ if ($newContain) {
+ $eagerLoader->contain($newContain);
+ }
+
+ foreach ($matching as $alias => $value) {
+ $eagerLoader->setMatching(
+ $options['aliasPath'] . '.' . $alias,
+ $value['queryBuilder'],
+ $value
+ );
+ }
+ }
+
+ /**
+ * Returns a single or multiple conditions to be appended to the generated join
+ * clause for getting the results on the target table.
+ *
+ * @param array $options list of options passed to attachTo method
+ * @return array
+ * @throws \RuntimeException if the number of columns in the foreignKey do not
+ * match the number of columns in the source table primaryKey
+ */
+ protected function _joinCondition(array $options): array
+ {
+ $conditions = [];
+ $tAlias = $this->_name;
+ $sAlias = $this->getSource()->getAlias();
+ $foreignKey = (array)$options['foreignKey'];
+ $bindingKey = (array)$this->getBindingKey();
+
+ if (count($foreignKey) !== count($bindingKey)) {
+ if (empty($bindingKey)) {
+ $table = $this->getTarget()->getTable();
+ if ($this->isOwningSide($this->getSource())) {
+ $table = $this->getSource()->getTable();
+ }
+ $msg = 'The "%s" table does not define a primary key, and cannot have join conditions generated.';
+ throw new RuntimeException(sprintf($msg, $table));
+ }
+
+ $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
+ throw new RuntimeException(sprintf(
+ $msg,
+ $this->_name,
+ implode(', ', $foreignKey),
+ implode(', ', $bindingKey)
+ ));
+ }
+
+ foreach ($foreignKey as $k => $f) {
+ $field = sprintf('%s.%s', $sAlias, $bindingKey[$k]);
+ $value = new IdentifierExpression(sprintf('%s.%s', $tAlias, $f));
+ $conditions[$field] = $value;
+ }
+
+ return $conditions;
+ }
+
+ /**
+ * Helper method to infer the requested finder and its options.
+ *
+ * Returns the inferred options from the finder $type.
+ *
+ * ### Examples:
+ *
+ * The following will call the finder 'translations' with the value of the finder as its options:
+ * $query->contain(['Comments' => ['finder' => ['translations']]]);
+ * $query->contain(['Comments' => ['finder' => ['translations' => []]]]);
+ * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]);
+ *
+ * @param string|array $finderData The finder name or an array having the name as key
+ * and options as value.
+ * @return array
+ */
+ protected function _extractFinder($finderData): array
+ {
+ $finderData = (array)$finderData;
+
+ if (is_numeric(key($finderData))) {
+ return [current($finderData), []];
+ }
+
+ return [key($finderData), current($finderData)];
+ }
+
+ /**
+ * Proxies property retrieval to the target table. This is handy for getting this
+ * association's associations
+ *
+ * @param string $property the property name
+ * @return \Cake\ORM\Association
+ * @throws \RuntimeException if no association with such name exists
+ */
+ public function __get($property)
+ {
+ return $this->getTarget()->{$property};
+ }
+
+ /**
+ * Proxies the isset call to the target table. This is handy to check if the
+ * target table has another association with the passed name
+ *
+ * @param string $property the property name
+ * @return bool true if the property exists
+ */
+ public function __isset($property)
+ {
+ return isset($this->getTarget()->{$property});
+ }
+
+ /**
+ * Proxies method calls to the target table.
+ *
+ * @param string $method name of the method to be invoked
+ * @param array $argument List of arguments passed to the function
+ * @return mixed
+ * @throws \BadMethodCallException
+ */
+ public function __call($method, $argument)
+ {
+ return $this->getTarget()->$method(...$argument);
+ }
+
+ /**
+ * Get the relationship type.
+ *
+ * @return string Constant of either ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY or MANY_TO_MANY.
+ */
+ abstract public function type(): string;
+
+ /**
+ * Eager loads a list of records in the target table that are related to another
+ * set of records in the source table. Source records can specified in two ways:
+ * first one is by passing a Query object setup to find on the source table and
+ * the other way is by explicitly passing an array of primary key values from
+ * the source table.
+ *
+ * The required way of passing related source records is controlled by "strategy"
+ * When the subquery strategy is used it will require a query on the source table.
+ * When using the select strategy, the list of primary keys will be used.
+ *
+ * Returns a closure that should be run for each record returned in a specific
+ * Query. This callable will be responsible for injecting the fields that are
+ * related to each specific passed row.
+ *
+ * Options array accepts the following keys:
+ *
+ * - query: Query object setup to find the source table records
+ * - keys: List of primary key values from the source table
+ * - foreignKey: The name of the field used to relate both tables
+ * - conditions: List of conditions to be passed to the query where() method
+ * - sort: The direction in which the records should be returned
+ * - fields: List of fields to select from the target table
+ * - contain: List of related tables to eager load associated to the target table
+ * - strategy: The name of strategy to use for finding target table records
+ * - nestKey: The array key under which results will be found when transforming the row
+ *
+ * @param array $options The options for eager loading.
+ * @return \Closure
+ */
+ abstract public function eagerLoader(array $options): Closure;
+
+ /**
+ * Handles cascading a delete from an associated model.
+ *
+ * Each implementing class should handle the cascaded delete as
+ * required.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete.
+ * @param array $options The options for the original delete.
+ * @return bool Success
+ */
+ abstract public function cascadeDelete(EntityInterface $entity, array $options = []): bool;
+
+ /**
+ * Returns whether or not the passed table is the owning side for this
+ * association. This means that rows in the 'target' table would miss important
+ * or required information if the row in 'source' did not exist.
+ *
+ * @param \Cake\ORM\Table $side The potential Table with ownership
+ * @return bool
+ */
+ abstract public function isOwningSide(Table $side): bool;
+
+ /**
+ * Extract the target's association data our from the passed entity and proxies
+ * the saving operation to the target table.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the data to be saved
+ * @param array $options The options for saving associated data.
+ * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns
+ * the saved entity
+ * @see \Cake\ORM\Table::save()
+ */
+ abstract public function saveAssociated(EntityInterface $entity, array $options = []);
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php
new file mode 100644
index 000000000..fb3541489
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsTo.php
@@ -0,0 +1,202 @@
+_foreignKey === null) {
+ $this->_foreignKey = $this->_modelKey($this->getTarget()->getAlias());
+ }
+
+ return $this->_foreignKey;
+ }
+
+ /**
+ * Handle cascading deletes.
+ *
+ * BelongsTo associations are never cleared in a cascading delete scenario.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete.
+ * @param array $options The options for the original delete.
+ * @return bool Success.
+ */
+ public function cascadeDelete(EntityInterface $entity, array $options = []): bool
+ {
+ return true;
+ }
+
+ /**
+ * Returns default property name based on association name.
+ *
+ * @return string
+ */
+ protected function _propertyName(): string
+ {
+ [, $name] = pluginSplit($this->_name);
+
+ return Inflector::underscore(Inflector::singularize($name));
+ }
+
+ /**
+ * Returns whether or not the passed table is the owning side for this
+ * association. This means that rows in the 'target' table would miss important
+ * or required information if the row in 'source' did not exist.
+ *
+ * @param \Cake\ORM\Table $side The potential Table with ownership
+ * @return bool
+ */
+ public function isOwningSide(Table $side): bool
+ {
+ return $side === $this->getTarget();
+ }
+
+ /**
+ * Get the relationship type.
+ *
+ * @return string
+ */
+ public function type(): string
+ {
+ return self::MANY_TO_ONE;
+ }
+
+ /**
+ * Takes an entity from the source table and looks if there is a field
+ * matching the property name for this association. The found entity will be
+ * saved on the target table for this association by passing supplied
+ * `$options`
+ *
+ * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
+ * @param array $options options to be passed to the save method in the target table
+ * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns
+ * the saved entity
+ * @see \Cake\ORM\Table::save()
+ */
+ public function saveAssociated(EntityInterface $entity, array $options = [])
+ {
+ $targetEntity = $entity->get($this->getProperty());
+ if (empty($targetEntity) || !($targetEntity instanceof EntityInterface)) {
+ return $entity;
+ }
+
+ $table = $this->getTarget();
+ $targetEntity = $table->save($targetEntity, $options);
+ if (!$targetEntity) {
+ return false;
+ }
+
+ $properties = array_combine(
+ (array)$this->getForeignKey(),
+ $targetEntity->extract((array)$this->getBindingKey())
+ );
+ $entity->set($properties, ['guard' => false]);
+
+ return $entity;
+ }
+
+ /**
+ * Returns a single or multiple conditions to be appended to the generated join
+ * clause for getting the results on the target table.
+ *
+ * @param array $options list of options passed to attachTo method
+ * @return \Cake\Database\Expression\IdentifierExpression[]
+ * @throws \RuntimeException if the number of columns in the foreignKey do not
+ * match the number of columns in the target table primaryKey
+ */
+ protected function _joinCondition(array $options): array
+ {
+ $conditions = [];
+ $tAlias = $this->_name;
+ $sAlias = $this->_sourceTable->getAlias();
+ $foreignKey = (array)$options['foreignKey'];
+ $bindingKey = (array)$this->getBindingKey();
+
+ if (count($foreignKey) !== count($bindingKey)) {
+ if (empty($bindingKey)) {
+ $msg = 'The "%s" table does not define a primary key. Please set one.';
+ throw new RuntimeException(sprintf($msg, $this->getTarget()->getTable()));
+ }
+
+ $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
+ throw new RuntimeException(sprintf(
+ $msg,
+ $this->_name,
+ implode(', ', $foreignKey),
+ implode(', ', $bindingKey)
+ ));
+ }
+
+ foreach ($foreignKey as $k => $f) {
+ $field = sprintf('%s.%s', $tAlias, $bindingKey[$k]);
+ $value = new IdentifierExpression(sprintf('%s.%s', $sAlias, $f));
+ $conditions[$field] = $value;
+ }
+
+ return $conditions;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function eagerLoader(array $options): Closure
+ {
+ $loader = new SelectLoader([
+ 'alias' => $this->getAlias(),
+ 'sourceAlias' => $this->getSource()->getAlias(),
+ 'targetAlias' => $this->getTarget()->getAlias(),
+ 'foreignKey' => $this->getForeignKey(),
+ 'bindingKey' => $this->getBindingKey(),
+ 'strategy' => $this->getStrategy(),
+ 'associationType' => $this->type(),
+ 'finder' => [$this, 'find'],
+ ]);
+
+ return $loader->buildEagerLoader($options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php
new file mode 100644
index 000000000..1ba41d9eb
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php
@@ -0,0 +1,1476 @@
+_targetForeignKey = $key;
+
+ return $this;
+ }
+
+ /**
+ * Gets the name of the field representing the foreign key to the target table.
+ *
+ * @return string|string[]
+ */
+ public function getTargetForeignKey()
+ {
+ if ($this->_targetForeignKey === null) {
+ $this->_targetForeignKey = $this->_modelKey($this->getTarget()->getAlias());
+ }
+
+ return $this->_targetForeignKey;
+ }
+
+ /**
+ * Whether this association can be expressed directly in a query join
+ *
+ * @param array $options custom options key that could alter the return value
+ * @return bool if the 'matching' key in $option is true then this function
+ * will return true, false otherwise
+ */
+ public function canBeJoined(array $options = []): bool
+ {
+ return !empty($options['matching']);
+ }
+
+ /**
+ * Gets the name of the field representing the foreign key to the source table.
+ *
+ * @return string|string[]
+ */
+ public function getForeignKey()
+ {
+ if ($this->_foreignKey === null) {
+ $this->_foreignKey = $this->_modelKey($this->getSource()->getTable());
+ }
+
+ return $this->_foreignKey;
+ }
+
+ /**
+ * Sets the sort order in which target records should be returned.
+ *
+ * @param mixed $sort A find() compatible order clause
+ * @return $this
+ */
+ public function setSort($sort)
+ {
+ $this->_sort = $sort;
+
+ return $this;
+ }
+
+ /**
+ * Gets the sort order in which target records should be returned.
+ *
+ * @return mixed
+ */
+ public function getSort()
+ {
+ return $this->_sort;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function defaultRowValue(array $row, bool $joined): array
+ {
+ $sourceAlias = $this->getSource()->getAlias();
+ if (isset($row[$sourceAlias])) {
+ $row[$sourceAlias][$this->getProperty()] = $joined ? null : [];
+ }
+
+ return $row;
+ }
+
+ /**
+ * Sets the table instance for the junction relation. If no arguments
+ * are passed, the current configured table instance is returned
+ *
+ * @param string|\Cake\ORM\Table|null $table Name or instance for the join table
+ * @return \Cake\ORM\Table
+ * @throws \InvalidArgumentException If the expected associations are incompatible with existing associations.
+ */
+ public function junction($table = null): Table
+ {
+ if ($table === null && $this->_junctionTable !== null) {
+ return $this->_junctionTable;
+ }
+
+ $tableLocator = $this->getTableLocator();
+ if ($table === null && $this->_through) {
+ $table = $this->_through;
+ } elseif ($table === null) {
+ $tableName = $this->_junctionTableName();
+ $tableAlias = Inflector::camelize($tableName);
+
+ $config = [];
+ if (!$tableLocator->exists($tableAlias)) {
+ $config = ['table' => $tableName, 'allowFallbackClass' => true];
+
+ // Propagate the connection if we'll get an auto-model
+ if (!App::className($tableAlias, 'Model/Table', 'Table')) {
+ $config['connection'] = $this->getSource()->getConnection();
+ }
+ }
+ $table = $tableLocator->get($tableAlias, $config);
+ }
+
+ if (is_string($table)) {
+ $table = $tableLocator->get($table);
+ }
+
+ $source = $this->getSource();
+ $target = $this->getTarget();
+ if ($source->getAlias() === $target->getAlias()) {
+ throw new InvalidArgumentException(sprintf(
+ 'The `%s` association on `%s` cannot target the same table.',
+ $this->getName(),
+ $source->getAlias()
+ ));
+ }
+
+ $this->_generateSourceAssociations($table, $source);
+ $this->_generateTargetAssociations($table, $source, $target);
+ $this->_generateJunctionAssociations($table, $source, $target);
+
+ return $this->_junctionTable = $table;
+ }
+
+ /**
+ * Generate reciprocal associations as necessary.
+ *
+ * Generates the following associations:
+ *
+ * - target hasMany junction e.g. Articles hasMany ArticlesTags
+ * - target belongsToMany source e.g Articles belongsToMany Tags.
+ *
+ * You can override these generated associations by defining associations
+ * with the correct aliases.
+ *
+ * @param \Cake\ORM\Table $junction The junction table.
+ * @param \Cake\ORM\Table $source The source table.
+ * @param \Cake\ORM\Table $target The target table.
+ * @return void
+ */
+ protected function _generateTargetAssociations(Table $junction, Table $source, Table $target): void
+ {
+ $junctionAlias = $junction->getAlias();
+ $sAlias = $source->getAlias();
+ $tAlias = $target->getAlias();
+
+ $targetBindingKey = null;
+ if ($junction->hasAssociation($tAlias)) {
+ $targetBindingKey = $junction->getAssociation($tAlias)->getBindingKey();
+ }
+
+ if (!$target->hasAssociation($junctionAlias)) {
+ $target->hasMany($junctionAlias, [
+ 'targetTable' => $junction,
+ 'bindingKey' => $targetBindingKey,
+ 'foreignKey' => $this->getTargetForeignKey(),
+ 'strategy' => $this->_strategy,
+ ]);
+ }
+ if (!$target->hasAssociation($sAlias)) {
+ $target->belongsToMany($sAlias, [
+ 'sourceTable' => $target,
+ 'targetTable' => $source,
+ 'foreignKey' => $this->getTargetForeignKey(),
+ 'targetForeignKey' => $this->getForeignKey(),
+ 'through' => $junction,
+ 'conditions' => $this->getConditions(),
+ 'strategy' => $this->_strategy,
+ ]);
+ }
+ }
+
+ /**
+ * Generate additional source table associations as necessary.
+ *
+ * Generates the following associations:
+ *
+ * - source hasMany junction e.g. Tags hasMany ArticlesTags
+ *
+ * You can override these generated associations by defining associations
+ * with the correct aliases.
+ *
+ * @param \Cake\ORM\Table $junction The junction table.
+ * @param \Cake\ORM\Table $source The source table.
+ * @return void
+ */
+ protected function _generateSourceAssociations(Table $junction, Table $source): void
+ {
+ $junctionAlias = $junction->getAlias();
+ $sAlias = $source->getAlias();
+
+ $sourceBindingKey = null;
+ if ($junction->hasAssociation($sAlias)) {
+ $sourceBindingKey = $junction->getAssociation($sAlias)->getBindingKey();
+ }
+
+ if (!$source->hasAssociation($junctionAlias)) {
+ $source->hasMany($junctionAlias, [
+ 'targetTable' => $junction,
+ 'bindingKey' => $sourceBindingKey,
+ 'foreignKey' => $this->getForeignKey(),
+ 'strategy' => $this->_strategy,
+ ]);
+ }
+ }
+
+ /**
+ * Generate associations on the junction table as necessary
+ *
+ * Generates the following associations:
+ *
+ * - junction belongsTo source e.g. ArticlesTags belongsTo Tags
+ * - junction belongsTo target e.g. ArticlesTags belongsTo Articles
+ *
+ * You can override these generated associations by defining associations
+ * with the correct aliases.
+ *
+ * @param \Cake\ORM\Table $junction The junction table.
+ * @param \Cake\ORM\Table $source The source table.
+ * @param \Cake\ORM\Table $target The target table.
+ * @return void
+ * @throws \InvalidArgumentException If the expected associations are incompatible with existing associations.
+ */
+ protected function _generateJunctionAssociations(Table $junction, Table $source, Table $target): void
+ {
+ $tAlias = $target->getAlias();
+ $sAlias = $source->getAlias();
+
+ if (!$junction->hasAssociation($tAlias)) {
+ $junction->belongsTo($tAlias, [
+ 'foreignKey' => $this->getTargetForeignKey(),
+ 'targetTable' => $target,
+ ]);
+ } else {
+ $belongsTo = $junction->getAssociation($tAlias);
+ if (
+ $this->getTargetForeignKey() !== $belongsTo->getForeignKey() ||
+ $target !== $belongsTo->getTarget()
+ ) {
+ throw new InvalidArgumentException(
+ "The existing `{$tAlias}` association on `{$junction->getAlias()}` " .
+ "is incompatible with the `{$this->getName()}` association on `{$source->getAlias()}`"
+ );
+ }
+ }
+
+ if (!$junction->hasAssociation($sAlias)) {
+ $junction->belongsTo($sAlias, [
+ 'foreignKey' => $this->getForeignKey(),
+ 'targetTable' => $source,
+ ]);
+ }
+ }
+
+ /**
+ * Alters a Query object to include the associated target table data in the final
+ * result
+ *
+ * The options array accept the following keys:
+ *
+ * - includeFields: Whether to include target model fields in the result or not
+ * - foreignKey: The name of the field to use as foreign key, if false none
+ * will be used
+ * - conditions: array with a list of conditions to filter the join with
+ * - fields: a list of fields in the target table to include in the result
+ * - type: The type of join to be used (e.g. INNER)
+ *
+ * @param \Cake\ORM\Query $query the query to be altered to include the target table data
+ * @param array $options Any extra options or overrides to be taken in account
+ * @return void
+ */
+ public function attachTo(Query $query, array $options = []): void
+ {
+ if (!empty($options['negateMatch'])) {
+ $this->_appendNotMatching($query, $options);
+
+ return;
+ }
+
+ $junction = $this->junction();
+ $belongsTo = $junction->getAssociation($this->getSource()->getAlias());
+ $cond = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]);
+ $cond += $this->junctionConditions();
+
+ $includeFields = null;
+ if (isset($options['includeFields'])) {
+ $includeFields = $options['includeFields'];
+ }
+
+ // Attach the junction table as well we need it to populate _joinData.
+ $assoc = $this->_targetTable->getAssociation($junction->getAlias());
+ $newOptions = array_intersect_key($options, ['joinType' => 1, 'fields' => 1]);
+ $newOptions += [
+ 'conditions' => $cond,
+ 'includeFields' => $includeFields,
+ 'foreignKey' => false,
+ ];
+ $assoc->attachTo($query, $newOptions);
+ $query->getEagerLoader()->addToJoinsMap($junction->getAlias(), $assoc, true);
+
+ parent::attachTo($query, $options);
+
+ $foreignKey = $this->getTargetForeignKey();
+ $thisJoin = $query->clause('join')[$this->getName()];
+ $thisJoin['conditions']->add($assoc->_joinCondition(['foreignKey' => $foreignKey]));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _appendNotMatching(Query $query, array $options): void
+ {
+ if (empty($options['negateMatch'])) {
+ return;
+ }
+ if (!isset($options['conditions'])) {
+ $options['conditions'] = [];
+ }
+ $junction = $this->junction();
+ $belongsTo = $junction->getAssociation($this->getSource()->getAlias());
+ $conds = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]);
+
+ $subquery = $this->find()
+ ->select(array_values($conds))
+ ->where($options['conditions']);
+
+ if (!empty($options['queryBuilder'])) {
+ $subquery = $options['queryBuilder']($subquery);
+ }
+
+ $subquery = $this->_appendJunctionJoin($subquery);
+
+ $query
+ ->andWhere(function (QueryExpression $exp) use ($subquery, $conds) {
+ $identifiers = [];
+ foreach (array_keys($conds) as $field) {
+ $identifiers[] = new IdentifierExpression($field);
+ }
+ $identifiers = $subquery->newExpr()->add($identifiers)->setConjunction(',');
+ $nullExp = clone $exp;
+
+ return $exp
+ ->or([
+ $exp->notIn($identifiers, $subquery),
+ $nullExp->and(array_map([$nullExp, 'isNull'], array_keys($conds))),
+ ]);
+ });
+ }
+
+ /**
+ * Get the relationship type.
+ *
+ * @return string
+ */
+ public function type(): string
+ {
+ return self::MANY_TO_MANY;
+ }
+
+ /**
+ * Return false as join conditions are defined in the junction table
+ *
+ * @param array $options list of options passed to attachTo method
+ * @return array
+ */
+ protected function _joinCondition(array $options): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function eagerLoader(array $options): Closure
+ {
+ $name = $this->_junctionAssociationName();
+ $loader = new SelectWithPivotLoader([
+ 'alias' => $this->getAlias(),
+ 'sourceAlias' => $this->getSource()->getAlias(),
+ 'targetAlias' => $this->getTarget()->getAlias(),
+ 'foreignKey' => $this->getForeignKey(),
+ 'bindingKey' => $this->getBindingKey(),
+ 'strategy' => $this->getStrategy(),
+ 'associationType' => $this->type(),
+ 'sort' => $this->getSort(),
+ 'junctionAssociationName' => $name,
+ 'junctionProperty' => $this->_junctionProperty,
+ 'junctionAssoc' => $this->getTarget()->getAssociation($name),
+ 'junctionConditions' => $this->junctionConditions(),
+ 'finder' => function () {
+ return $this->_appendJunctionJoin($this->find(), []);
+ },
+ ]);
+
+ return $loader->buildEagerLoader($options);
+ }
+
+ /**
+ * Clear out the data in the junction table for a given entity.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascading delete.
+ * @param array $options The options for the original delete.
+ * @return bool Success.
+ */
+ public function cascadeDelete(EntityInterface $entity, array $options = []): bool
+ {
+ if (!$this->getDependent()) {
+ return true;
+ }
+ $foreignKey = (array)$this->getForeignKey();
+ $bindingKey = (array)$this->getBindingKey();
+ $conditions = [];
+
+ if (!empty($bindingKey)) {
+ $conditions = array_combine($foreignKey, $entity->extract($bindingKey));
+ }
+
+ $table = $this->junction();
+ $hasMany = $this->getSource()->getAssociation($table->getAlias());
+ if ($this->_cascadeCallbacks) {
+ foreach ($hasMany->find('all')->where($conditions)->all()->toList() as $related) {
+ $success = $table->delete($related, $options);
+ if (!$success) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ $assocConditions = $hasMany->getConditions();
+ if (is_array($assocConditions)) {
+ $conditions = array_merge($conditions, $assocConditions);
+ } else {
+ $conditions[] = $assocConditions;
+ }
+
+ $table->deleteAll($conditions);
+
+ return true;
+ }
+
+ /**
+ * Returns boolean true, as both of the tables 'own' rows in the other side
+ * of the association via the joint table.
+ *
+ * @param \Cake\ORM\Table $side The potential Table with ownership
+ * @return bool
+ */
+ public function isOwningSide(Table $side): bool
+ {
+ return true;
+ }
+
+ /**
+ * Sets the strategy that should be used for saving.
+ *
+ * @param string $strategy the strategy name to be used
+ * @throws \InvalidArgumentException if an invalid strategy name is passed
+ * @return $this
+ */
+ public function setSaveStrategy(string $strategy)
+ {
+ if (!in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE], true)) {
+ $msg = sprintf('Invalid save strategy "%s"', $strategy);
+ throw new InvalidArgumentException($msg);
+ }
+
+ $this->_saveStrategy = $strategy;
+
+ return $this;
+ }
+
+ /**
+ * Gets the strategy that should be used for saving.
+ *
+ * @return string the strategy to be used for saving
+ */
+ public function getSaveStrategy(): string
+ {
+ return $this->_saveStrategy;
+ }
+
+ /**
+ * Takes an entity from the source table and looks if there is a field
+ * matching the property name for this association. The found entity will be
+ * saved on the target table for this association by passing supplied
+ * `$options`
+ *
+ * When using the 'append' strategy, this function will only create new links
+ * between each side of this association. It will not destroy existing ones even
+ * though they may not be present in the array of entities to be saved.
+ *
+ * When using the 'replace' strategy, existing links will be removed and new links
+ * will be created in the joint table. If there exists links in the database to some
+ * of the entities intended to be saved by this method, they will be updated,
+ * not deleted.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
+ * @param array $options options to be passed to the save method in the target table
+ * @throws \InvalidArgumentException if the property representing the association
+ * in the parent entity cannot be traversed
+ * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns
+ * the saved entity
+ * @see \Cake\ORM\Table::save()
+ * @see \Cake\ORM\Association\BelongsToMany::replaceLinks()
+ */
+ public function saveAssociated(EntityInterface $entity, array $options = [])
+ {
+ $targetEntity = $entity->get($this->getProperty());
+ $strategy = $this->getSaveStrategy();
+
+ $isEmpty = in_array($targetEntity, [null, [], '', false], true);
+ if ($isEmpty && $entity->isNew()) {
+ return $entity;
+ }
+ if ($isEmpty) {
+ $targetEntity = [];
+ }
+
+ if ($strategy === self::SAVE_APPEND) {
+ return $this->_saveTarget($entity, $targetEntity, $options);
+ }
+
+ if ($this->replaceLinks($entity, $targetEntity, $options)) {
+ return $entity;
+ }
+
+ return false;
+ }
+
+ /**
+ * Persists each of the entities into the target table and creates links between
+ * the parent entity and each one of the saved target entities.
+ *
+ * @param \Cake\Datasource\EntityInterface $parentEntity the source entity containing the target
+ * entities to be saved.
+ * @param array $entities list of entities to persist in target table and to
+ * link to the parent entity
+ * @param array $options list of options accepted by `Table::save()`
+ * @throws \InvalidArgumentException if the property representing the association
+ * in the parent entity cannot be traversed
+ * @return \Cake\Datasource\EntityInterface|false The parent entity after all links have been
+ * created if no errors happened, false otherwise
+ */
+ protected function _saveTarget(EntityInterface $parentEntity, array $entities, $options)
+ {
+ $joinAssociations = false;
+ if (!empty($options['associated'][$this->_junctionProperty]['associated'])) {
+ $joinAssociations = $options['associated'][$this->_junctionProperty]['associated'];
+ }
+ unset($options['associated'][$this->_junctionProperty]);
+
+ $table = $this->getTarget();
+ $original = $entities;
+ $persisted = [];
+
+ foreach ($entities as $k => $entity) {
+ if (!($entity instanceof EntityInterface)) {
+ break;
+ }
+
+ if (!empty($options['atomic'])) {
+ $entity = clone $entity;
+ }
+
+ $saved = $table->save($entity, $options);
+ if ($saved) {
+ $entities[$k] = $entity;
+ $persisted[] = $entity;
+ continue;
+ }
+
+ // Saving the new linked entity failed, copy errors back into the
+ // original entity if applicable and abort.
+ if (!empty($options['atomic'])) {
+ $original[$k]->setErrors($entity->getErrors());
+ }
+ if ($saved === false) {
+ return false;
+ }
+ }
+
+ $options['associated'] = $joinAssociations;
+ $success = $this->_saveLinks($parentEntity, $persisted, $options);
+ if (!$success && !empty($options['atomic'])) {
+ $parentEntity->set($this->getProperty(), $original);
+
+ return false;
+ }
+
+ $parentEntity->set($this->getProperty(), $entities);
+
+ return $parentEntity;
+ }
+
+ /**
+ * Creates links between the source entity and each of the passed target entities
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity the entity from source table in this
+ * association
+ * @param \Cake\Datasource\EntityInterface[] $targetEntities list of entities to link to link to the source entity using the
+ * junction table
+ * @param array $options list of options accepted by `Table::save()`
+ * @return bool success
+ */
+ protected function _saveLinks(EntityInterface $sourceEntity, array $targetEntities, array $options): bool
+ {
+ $target = $this->getTarget();
+ $junction = $this->junction();
+ $entityClass = $junction->getEntityClass();
+ $belongsTo = $junction->getAssociation($target->getAlias());
+ $foreignKey = (array)$this->getForeignKey();
+ $assocForeignKey = (array)$belongsTo->getForeignKey();
+ $targetBindingKey = (array)$belongsTo->getBindingKey();
+ $bindingKey = (array)$this->getBindingKey();
+ $jointProperty = $this->_junctionProperty;
+ $junctionRegistryAlias = $junction->getRegistryAlias();
+
+ foreach ($targetEntities as $e) {
+ $joint = $e->get($jointProperty);
+ if (!$joint || !($joint instanceof EntityInterface)) {
+ $joint = new $entityClass([], ['markNew' => true, 'source' => $junctionRegistryAlias]);
+ }
+ $sourceKeys = array_combine($foreignKey, $sourceEntity->extract($bindingKey));
+ $targetKeys = array_combine($assocForeignKey, $e->extract($targetBindingKey));
+
+ $changedKeys = (
+ $sourceKeys !== $joint->extract($foreignKey) ||
+ $targetKeys !== $joint->extract($assocForeignKey)
+ );
+ // Keys were changed, the junction table record _could_ be
+ // new. By clearing the primary key values, and marking the entity
+ // as new, we let save() sort out whether or not we have a new link
+ // or if we are updating an existing link.
+ if ($changedKeys) {
+ $joint->setNew(true);
+ $joint->unset($junction->getPrimaryKey())
+ ->set(array_merge($sourceKeys, $targetKeys), ['guard' => false]);
+ }
+ $saved = $junction->save($joint, $options);
+
+ if (!$saved && !empty($options['atomic'])) {
+ return false;
+ }
+
+ $e->set($jointProperty, $joint);
+ $e->setDirty($jointProperty, false);
+ }
+
+ return true;
+ }
+
+ /**
+ * Associates the source entity to each of the target entities provided by
+ * creating links in the junction table. Both the source entity and each of
+ * the target entities are assumed to be already persisted, if they are marked
+ * as new or their status is unknown then an exception will be thrown.
+ *
+ * When using this method, all entities in `$targetEntities` will be appended to
+ * the source entity's property corresponding to this association object.
+ *
+ * This method does not check link uniqueness.
+ *
+ * ### Example:
+ *
+ * ```
+ * $newTags = $tags->find('relevant')->toArray();
+ * $articles->getAssociation('tags')->link($article, $newTags);
+ * ```
+ *
+ * `$article->get('tags')` will contain all tags in `$newTags` after liking
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
+ * of this association
+ * @param \Cake\Datasource\EntityInterface[] $targetEntities list of entities belonging to the `target` side
+ * of this association
+ * @param array $options list of options to be passed to the internal `save` call
+ * @throws \InvalidArgumentException when any of the values in $targetEntities is
+ * detected to not be already persisted
+ * @return bool true on success, false otherwise
+ */
+ public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool
+ {
+ $this->_checkPersistenceStatus($sourceEntity, $targetEntities);
+ $property = $this->getProperty();
+ $links = $sourceEntity->get($property) ?: [];
+ $links = array_merge($links, $targetEntities);
+ $sourceEntity->set($property, $links);
+
+ return $this->junction()->getConnection()->transactional(
+ function () use ($sourceEntity, $targetEntities, $options) {
+ return $this->_saveLinks($sourceEntity, $targetEntities, $options);
+ }
+ );
+ }
+
+ /**
+ * Removes all links between the passed source entity and each of the provided
+ * target entities. This method assumes that all passed objects are already persisted
+ * in the database and that each of them contain a primary key value.
+ *
+ * ### Options
+ *
+ * Additionally to the default options accepted by `Table::delete()`, the following
+ * keys are supported:
+ *
+ * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that
+ * are stored in `$sourceEntity` (default: true)
+ *
+ * By default this method will unset each of the entity objects stored inside the
+ * source entity.
+ *
+ * ### Example:
+ *
+ * ```
+ * $article->tags = [$tag1, $tag2, $tag3, $tag4];
+ * $tags = [$tag1, $tag2, $tag3];
+ * $articles->getAssociation('tags')->unlink($article, $tags);
+ * ```
+ *
+ * `$article->get('tags')` will contain only `[$tag4]` after deleting in the database
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity An entity persisted in the source table for
+ * this association.
+ * @param \Cake\Datasource\EntityInterface[] $targetEntities List of entities persisted in the target table for
+ * this association.
+ * @param array|bool $options List of options to be passed to the internal `delete` call,
+ * or a `boolean` as `cleanProperty` key shortcut.
+ * @throws \InvalidArgumentException If non persisted entities are passed or if
+ * any of them is lacking a primary key value.
+ * @return bool Success
+ */
+ public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = []): bool
+ {
+ if (is_bool($options)) {
+ $options = [
+ 'cleanProperty' => $options,
+ ];
+ } else {
+ $options += ['cleanProperty' => true];
+ }
+
+ $this->_checkPersistenceStatus($sourceEntity, $targetEntities);
+ $property = $this->getProperty();
+
+ $this->junction()->getConnection()->transactional(
+ function () use ($sourceEntity, $targetEntities, $options): void {
+ $links = $this->_collectJointEntities($sourceEntity, $targetEntities);
+ foreach ($links as $entity) {
+ $this->_junctionTable->delete($entity, $options);
+ }
+ }
+ );
+
+ $existing = $sourceEntity->get($property) ?: [];
+ if (!$options['cleanProperty'] || empty($existing)) {
+ return true;
+ }
+
+ /** @var \SplObjectStorage<\Cake\Datasource\EntityInterface, null> $storage*/
+ $storage = new SplObjectStorage();
+ foreach ($targetEntities as $e) {
+ $storage->attach($e);
+ }
+
+ foreach ($existing as $k => $e) {
+ if ($storage->contains($e)) {
+ unset($existing[$k]);
+ }
+ }
+
+ $sourceEntity->set($property, array_values($existing));
+ $sourceEntity->setDirty($property, false);
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setConditions($conditions)
+ {
+ parent::setConditions($conditions);
+ $this->_targetConditions = $this->_junctionConditions = null;
+
+ return $this;
+ }
+
+ /**
+ * Sets the current join table, either the name of the Table instance or the instance itself.
+ *
+ * @param string|\Cake\ORM\Table $through Name of the Table instance or the instance itself
+ * @return $this
+ */
+ public function setThrough($through)
+ {
+ $this->_through = $through;
+
+ return $this;
+ }
+
+ /**
+ * Gets the current join table, either the name of the Table instance or the instance itself.
+ *
+ * @return string|\Cake\ORM\Table
+ */
+ public function getThrough()
+ {
+ return $this->_through;
+ }
+
+ /**
+ * Returns filtered conditions that reference the target table.
+ *
+ * Any string expressions, or expression objects will
+ * also be returned in this list.
+ *
+ * @return mixed Generally an array. If the conditions
+ * are not an array, the association conditions will be
+ * returned unmodified.
+ */
+ protected function targetConditions()
+ {
+ if ($this->_targetConditions !== null) {
+ return $this->_targetConditions;
+ }
+ $conditions = $this->getConditions();
+ if (!is_array($conditions)) {
+ return $conditions;
+ }
+ $matching = [];
+ $alias = $this->getAlias() . '.';
+ foreach ($conditions as $field => $value) {
+ if (is_string($field) && strpos($field, $alias) === 0) {
+ $matching[$field] = $value;
+ } elseif (is_int($field) || $value instanceof ExpressionInterface) {
+ $matching[$field] = $value;
+ }
+ }
+
+ return $this->_targetConditions = $matching;
+ }
+
+ /**
+ * Returns filtered conditions that specifically reference
+ * the junction table.
+ *
+ * @return array
+ */
+ protected function junctionConditions(): array
+ {
+ if ($this->_junctionConditions !== null) {
+ return $this->_junctionConditions;
+ }
+ $matching = [];
+ $conditions = $this->getConditions();
+ if (!is_array($conditions)) {
+ return $matching;
+ }
+ $alias = $this->_junctionAssociationName() . '.';
+ foreach ($conditions as $field => $value) {
+ $isString = is_string($field);
+ if ($isString && strpos($field, $alias) === 0) {
+ $matching[$field] = $value;
+ }
+ // Assume that operators contain junction conditions.
+ // Trying to manage complex conditions could result in incorrect queries.
+ if ($isString && in_array(strtoupper($field), ['OR', 'NOT', 'AND', 'XOR'], true)) {
+ $matching[$field] = $value;
+ }
+ }
+
+ return $this->_junctionConditions = $matching;
+ }
+
+ /**
+ * Proxies the finding operation to the target table's find method
+ * and modifies the query accordingly based of this association
+ * configuration.
+ *
+ * If your association includes conditions or a finder, the junction table will be
+ * included in the query's contained associations.
+ *
+ * @param string|array|null $type the type of query to perform, if an array is passed,
+ * it will be interpreted as the `$options` parameter
+ * @param array $options The options to for the find
+ * @see \Cake\ORM\Table::find()
+ * @return \Cake\ORM\Query
+ */
+ public function find($type = null, array $options = []): Query
+ {
+ $type = $type ?: $this->getFinder();
+ [$type, $opts] = $this->_extractFinder($type);
+ $query = $this->getTarget()
+ ->find($type, $options + $opts)
+ ->where($this->targetConditions())
+ ->addDefaultTypes($this->getTarget());
+
+ if ($this->junctionConditions()) {
+ return $this->_appendJunctionJoin($query);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Append a join to the junction table.
+ *
+ * @param \Cake\ORM\Query $query The query to append.
+ * @param array|null $conditions The query conditions to use.
+ * @return \Cake\ORM\Query The modified query.
+ */
+ protected function _appendJunctionJoin(Query $query, ?array $conditions = null): Query
+ {
+ $junctionTable = $this->junction();
+ if ($conditions === null) {
+ $belongsTo = $junctionTable->getAssociation($this->getTarget()->getAlias());
+ $conditions = $belongsTo->_joinCondition([
+ 'foreignKey' => $this->getTargetForeignKey(),
+ ]);
+ $conditions += $this->junctionConditions();
+ }
+
+ $name = $this->_junctionAssociationName();
+ /** @var array $joins */
+ $joins = $query->clause('join');
+ $matching = [
+ $name => [
+ 'table' => $junctionTable->getTable(),
+ 'conditions' => $conditions,
+ 'type' => Query::JOIN_TYPE_INNER,
+ ],
+ ];
+
+ $query
+ ->addDefaultTypes($junctionTable)
+ ->join($matching + $joins, [], true);
+
+ return $query;
+ }
+
+ /**
+ * Replaces existing association links between the source entity and the target
+ * with the ones passed. This method does a smart cleanup, links that are already
+ * persisted and present in `$targetEntities` will not be deleted, new links will
+ * be created for the passed target entities that are not already in the database
+ * and the rest will be removed.
+ *
+ * For example, if an article is linked to tags 'cake' and 'framework' and you pass
+ * to this method an array containing the entities for tags 'cake', 'php' and 'awesome',
+ * only the link for cake will be kept in database, the link for 'framework' will be
+ * deleted and the links for 'php' and 'awesome' will be created.
+ *
+ * Existing links are not deleted and created again, they are either left untouched
+ * or updated so that potential extra information stored in the joint row is not
+ * lost. Updating the link row can be done by making sure the corresponding passed
+ * target entity contains the joint property with its primary key and any extra
+ * information to be stored.
+ *
+ * On success, the passed `$sourceEntity` will contain `$targetEntities` as value
+ * in the corresponding property for this association.
+ *
+ * This method assumes that links between both the source entity and each of the
+ * target entities are unique. That is, for any given row in the source table there
+ * can only be one link in the junction table pointing to any other given row in
+ * the target table.
+ *
+ * Additional options for new links to be saved can be passed in the third argument,
+ * check `Table::save()` for information on the accepted options.
+ *
+ * ### Example:
+ *
+ * ```
+ * $article->tags = [$tag1, $tag2, $tag3, $tag4];
+ * $articles->save($article);
+ * $tags = [$tag1, $tag3];
+ * $articles->getAssociation('tags')->replaceLinks($article, $tags);
+ * ```
+ *
+ * `$article->get('tags')` will contain only `[$tag1, $tag3]` at the end
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
+ * this association
+ * @param array $targetEntities list of entities from the target table to be linked
+ * @param array $options list of options to be passed to the internal `save`/`delete` calls
+ * when persisting/updating new links, or deleting existing ones
+ * @throws \InvalidArgumentException if non persisted entities are passed or if
+ * any of them is lacking a primary key value
+ * @return bool success
+ */
+ public function replaceLinks(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool
+ {
+ $bindingKey = (array)$this->getBindingKey();
+ $primaryValue = $sourceEntity->extract($bindingKey);
+
+ if (count(array_filter($primaryValue, 'strlen')) !== count($bindingKey)) {
+ $message = 'Could not find primary key value for source entity';
+ throw new InvalidArgumentException($message);
+ }
+
+ return $this->junction()->getConnection()->transactional(
+ function () use ($sourceEntity, $targetEntities, $primaryValue, $options) {
+ $junction = $this->junction();
+ $target = $this->getTarget();
+
+ $foreignKey = (array)$this->getForeignKey();
+ $prefixedForeignKey = array_map([$junction, 'aliasField'], $foreignKey);
+
+ $junctionPrimaryKey = (array)$junction->getPrimaryKey();
+ $assocForeignKey = (array)$junction->getAssociation($target->getAlias())->getForeignKey();
+
+ $keys = array_combine($foreignKey, $prefixedForeignKey);
+ foreach (array_merge($assocForeignKey, $junctionPrimaryKey) as $key) {
+ $keys[$key] = $junction->aliasField($key);
+ }
+
+ // Find existing rows so that we can diff with new entities.
+ // Only hydrate primary/foreign key columns to save time.
+ // Attach joins first to ensure where conditions have correct
+ // column types set.
+ $existing = $this->_appendJunctionJoin($this->find())
+ ->select($keys)
+ ->where(array_combine($prefixedForeignKey, $primaryValue));
+
+ // Because we're aliasing key fields to look like they are not
+ // from joined table we need to overwrite the type map as the junction
+ // table can have a surrogate primary key that doesn't share a type
+ // with the target table.
+ $junctionTypes = array_intersect_key($junction->getSchema()->typeMap(), $keys);
+ $existing->getSelectTypeMap()->setTypes($junctionTypes);
+
+ $jointEntities = $this->_collectJointEntities($sourceEntity, $targetEntities);
+ $inserts = $this->_diffLinks($existing, $jointEntities, $targetEntities, $options);
+ if ($inserts === false) {
+ return false;
+ }
+
+ if ($inserts && !$this->_saveTarget($sourceEntity, $inserts, $options)) {
+ return false;
+ }
+
+ $property = $this->getProperty();
+
+ if (count($inserts)) {
+ $inserted = array_combine(
+ array_keys($inserts),
+ (array)$sourceEntity->get($property)
+ ) ?: [];
+ $targetEntities = $inserted + $targetEntities;
+ }
+
+ ksort($targetEntities);
+ $sourceEntity->set($property, array_values($targetEntities));
+ $sourceEntity->setDirty($property, false);
+
+ return true;
+ }
+ );
+ }
+
+ /**
+ * Helper method used to delete the difference between the links passed in
+ * `$existing` and `$jointEntities`. This method will return the values from
+ * `$targetEntities` that were not deleted from calculating the difference.
+ *
+ * @param \Cake\ORM\Query $existing a query for getting existing links
+ * @param \Cake\Datasource\EntityInterface[] $jointEntities link entities that should be persisted
+ * @param array $targetEntities entities in target table that are related to
+ * the `$jointEntities`
+ * @param array $options list of options accepted by `Table::delete()`
+ * @return array|false Array of entities not deleted or false in case of deletion failure for atomic saves.
+ */
+ protected function _diffLinks(
+ Query $existing,
+ array $jointEntities,
+ array $targetEntities,
+ array $options = []
+ ) {
+ $junction = $this->junction();
+ $target = $this->getTarget();
+ $belongsTo = $junction->getAssociation($target->getAlias());
+ $foreignKey = (array)$this->getForeignKey();
+ $assocForeignKey = (array)$belongsTo->getForeignKey();
+
+ $keys = array_merge($foreignKey, $assocForeignKey);
+ $deletes = $indexed = $present = [];
+
+ foreach ($jointEntities as $i => $entity) {
+ $indexed[$i] = $entity->extract($keys);
+ $present[$i] = array_values($entity->extract($assocForeignKey));
+ }
+
+ foreach ($existing as $result) {
+ $fields = $result->extract($keys);
+ $found = false;
+ foreach ($indexed as $i => $data) {
+ if ($fields === $data) {
+ unset($indexed[$i]);
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $deletes[] = $result;
+ }
+ }
+
+ $primary = (array)$target->getPrimaryKey();
+ $jointProperty = $this->_junctionProperty;
+ foreach ($targetEntities as $k => $entity) {
+ if (!($entity instanceof EntityInterface)) {
+ continue;
+ }
+ $key = array_values($entity->extract($primary));
+ foreach ($present as $i => $data) {
+ if ($key === $data && !$entity->get($jointProperty)) {
+ unset($targetEntities[$k], $present[$i]);
+ break;
+ }
+ }
+ }
+
+ foreach ($deletes as $entity) {
+ if (!$junction->delete($entity, $options) && !empty($options['atomic'])) {
+ return false;
+ }
+ }
+
+ return $targetEntities;
+ }
+
+ /**
+ * Throws an exception should any of the passed entities is not persisted.
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
+ * of this association
+ * @param \Cake\Datasource\EntityInterface[] $targetEntities list of entities belonging to the `target` side
+ * of this association
+ * @return bool
+ * @throws \InvalidArgumentException
+ */
+ protected function _checkPersistenceStatus(EntityInterface $sourceEntity, array $targetEntities): bool
+ {
+ if ($sourceEntity->isNew()) {
+ $error = 'Source entity needs to be persisted before links can be created or removed.';
+ throw new InvalidArgumentException($error);
+ }
+
+ foreach ($targetEntities as $entity) {
+ if ($entity->isNew()) {
+ $error = 'Cannot link entities that have not been persisted yet.';
+ throw new InvalidArgumentException($error);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the list of joint entities that exist between the source entity
+ * and each of the passed target entities
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity The row belonging to the source side
+ * of this association.
+ * @param array $targetEntities The rows belonging to the target side of this
+ * association.
+ * @throws \InvalidArgumentException if any of the entities is lacking a primary
+ * key value
+ * @return \Cake\Datasource\EntityInterface[]
+ */
+ protected function _collectJointEntities(EntityInterface $sourceEntity, array $targetEntities): array
+ {
+ $target = $this->getTarget();
+ $source = $this->getSource();
+ $junction = $this->junction();
+ $jointProperty = $this->_junctionProperty;
+ $primary = (array)$target->getPrimaryKey();
+
+ $result = [];
+ $missing = [];
+
+ foreach ($targetEntities as $entity) {
+ if (!($entity instanceof EntityInterface)) {
+ continue;
+ }
+ $joint = $entity->get($jointProperty);
+
+ if (!$joint || !($joint instanceof EntityInterface)) {
+ $missing[] = $entity->extract($primary);
+ continue;
+ }
+
+ $result[] = $joint;
+ }
+
+ if (empty($missing)) {
+ return $result;
+ }
+
+ $belongsTo = $junction->getAssociation($target->getAlias());
+ $hasMany = $source->getAssociation($junction->getAlias());
+ $foreignKey = (array)$this->getForeignKey();
+ $foreignKey = array_map(function ($key) {
+ return $key . ' IS';
+ }, $foreignKey);
+ $assocForeignKey = (array)$belongsTo->getForeignKey();
+ $assocForeignKey = array_map(function ($key) {
+ return $key . ' IS';
+ }, $assocForeignKey);
+ $sourceKey = $sourceEntity->extract((array)$source->getPrimaryKey());
+
+ $unions = [];
+ foreach ($missing as $key) {
+ $unions[] = $hasMany->find()
+ ->where(array_combine($foreignKey, $sourceKey))
+ ->where(array_combine($assocForeignKey, $key));
+ }
+
+ $query = array_shift($unions);
+ foreach ($unions as $q) {
+ $query->union($q);
+ }
+
+ return array_merge($result, $query->toArray());
+ }
+
+ /**
+ * Returns the name of the association from the target table to the junction table,
+ * this name is used to generate alias in the query and to later on retrieve the
+ * results.
+ *
+ * @return string
+ */
+ protected function _junctionAssociationName(): string
+ {
+ if (!$this->_junctionAssociationName) {
+ $this->_junctionAssociationName = $this->getTarget()
+ ->getAssociation($this->junction()->getAlias())
+ ->getName();
+ }
+
+ return $this->_junctionAssociationName;
+ }
+
+ /**
+ * Sets the name of the junction table.
+ * If no arguments are passed the current configured name is returned. A default
+ * name based of the associated tables will be generated if none found.
+ *
+ * @param string|null $name The name of the junction table.
+ * @return string
+ */
+ protected function _junctionTableName(?string $name = null): string
+ {
+ if ($name === null) {
+ if (empty($this->_junctionTableName)) {
+ $tablesNames = array_map('Cake\Utility\Inflector::underscore', [
+ $this->getSource()->getTable(),
+ $this->getTarget()->getTable(),
+ ]);
+ sort($tablesNames);
+ $this->_junctionTableName = implode('_', $tablesNames);
+ }
+
+ return $this->_junctionTableName;
+ }
+
+ return $this->_junctionTableName = $name;
+ }
+
+ /**
+ * Parse extra options passed in the constructor.
+ *
+ * @param array $options original list of options passed in constructor
+ * @return void
+ */
+ protected function _options(array $options): void
+ {
+ if (!empty($options['targetForeignKey'])) {
+ $this->setTargetForeignKey($options['targetForeignKey']);
+ }
+ if (!empty($options['joinTable'])) {
+ $this->_junctionTableName($options['joinTable']);
+ }
+ if (!empty($options['through'])) {
+ $this->setThrough($options['through']);
+ }
+ if (!empty($options['saveStrategy'])) {
+ $this->setSaveStrategy($options['saveStrategy']);
+ }
+ if (isset($options['sort'])) {
+ $this->setSort($options['sort']);
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteHelper.php b/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteHelper.php
new file mode 100644
index 000000000..1b7a7d804
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/DependentDeleteHelper.php
@@ -0,0 +1,65 @@
+getDependent()) {
+ return true;
+ }
+ $table = $association->getTarget();
+ /** @psalm-suppress InvalidArgument */
+ $foreignKey = array_map([$association, 'aliasField'], (array)$association->getForeignKey());
+ $bindingKey = (array)$association->getBindingKey();
+ $conditions = array_combine($foreignKey, $entity->extract($bindingKey));
+
+ if ($association->getCascadeCallbacks()) {
+ foreach ($association->find()->where($conditions)->all()->toList() as $related) {
+ $success = $table->delete($related, $options);
+ if (!$success) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ $association->deleteAll($conditions);
+
+ return true;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php b/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php
new file mode 100644
index 000000000..fa59e21f7
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/HasMany.php
@@ -0,0 +1,682 @@
+getSource();
+ }
+
+ /**
+ * Sets the strategy that should be used for saving.
+ *
+ * @param string $strategy the strategy name to be used
+ * @throws \InvalidArgumentException if an invalid strategy name is passed
+ * @return $this
+ */
+ public function setSaveStrategy(string $strategy)
+ {
+ if (!in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE], true)) {
+ $msg = sprintf('Invalid save strategy "%s"', $strategy);
+ throw new InvalidArgumentException($msg);
+ }
+
+ $this->_saveStrategy = $strategy;
+
+ return $this;
+ }
+
+ /**
+ * Gets the strategy that should be used for saving.
+ *
+ * @return string the strategy to be used for saving
+ */
+ public function getSaveStrategy(): string
+ {
+ return $this->_saveStrategy;
+ }
+
+ /**
+ * Takes an entity from the source table and looks if there is a field
+ * matching the property name for this association. The found entity will be
+ * saved on the target table for this association by passing supplied
+ * `$options`
+ *
+ * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
+ * @param array $options options to be passed to the save method in the target table
+ * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns
+ * the saved entity
+ * @see \Cake\ORM\Table::save()
+ * @throws \InvalidArgumentException when the association data cannot be traversed.
+ */
+ public function saveAssociated(EntityInterface $entity, array $options = [])
+ {
+ $targetEntities = $entity->get($this->getProperty());
+
+ $isEmpty = in_array($targetEntities, [null, [], '', false], true);
+ if ($isEmpty) {
+ if (
+ $entity->isNew() ||
+ $this->getSaveStrategy() !== self::SAVE_REPLACE
+ ) {
+ return $entity;
+ }
+
+ $targetEntities = [];
+ }
+
+ if (!is_iterable($targetEntities)) {
+ $name = $this->getProperty();
+ $message = sprintf('Could not save %s, it cannot be traversed', $name);
+ throw new InvalidArgumentException($message);
+ }
+
+ $foreignKeyReference = array_combine(
+ (array)$this->getForeignKey(),
+ $entity->extract((array)$this->getBindingKey())
+ );
+
+ $options['_sourceTable'] = $this->getSource();
+
+ if (
+ $this->_saveStrategy === self::SAVE_REPLACE &&
+ !$this->_unlinkAssociated($foreignKeyReference, $entity, $this->getTarget(), $targetEntities, $options)
+ ) {
+ return false;
+ }
+
+ if (!is_array($targetEntities)) {
+ $targetEntities = iterator_to_array($targetEntities);
+ }
+ if (!$this->_saveTarget($foreignKeyReference, $entity, $targetEntities, $options)) {
+ return false;
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Persists each of the entities into the target table and creates links between
+ * the parent entity and each one of the saved target entities.
+ *
+ * @param array $foreignKeyReference The foreign key reference defining the link between the
+ * target entity, and the parent entity.
+ * @param \Cake\Datasource\EntityInterface $parentEntity The source entity containing the target
+ * entities to be saved.
+ * @param array $entities list of entities
+ * to persist in target table and to link to the parent entity
+ * @param array $options list of options accepted by `Table::save()`.
+ * @return bool `true` on success, `false` otherwise.
+ */
+ protected function _saveTarget(
+ array $foreignKeyReference,
+ EntityInterface $parentEntity,
+ array $entities,
+ array $options
+ ): bool {
+ $foreignKey = array_keys($foreignKeyReference);
+ $table = $this->getTarget();
+ $original = $entities;
+
+ foreach ($entities as $k => $entity) {
+ if (!($entity instanceof EntityInterface)) {
+ break;
+ }
+
+ if (!empty($options['atomic'])) {
+ $entity = clone $entity;
+ }
+
+ if ($foreignKeyReference !== $entity->extract($foreignKey)) {
+ $entity->set($foreignKeyReference, ['guard' => false]);
+ }
+
+ if ($table->save($entity, $options)) {
+ $entities[$k] = $entity;
+ continue;
+ }
+
+ if (!empty($options['atomic'])) {
+ $original[$k]->setErrors($entity->getErrors());
+ if ($entity instanceof InvalidPropertyInterface) {
+ $original[$k]->setInvalid($entity->getInvalid());
+ }
+
+ return false;
+ }
+ }
+
+ $parentEntity->set($this->getProperty(), $entities);
+
+ return true;
+ }
+
+ /**
+ * Associates the source entity to each of the target entities provided.
+ * When using this method, all entities in `$targetEntities` will be appended to
+ * the source entity's property corresponding to this association object.
+ *
+ * This method does not check link uniqueness.
+ * Changes are persisted in the database and also in the source entity.
+ *
+ * ### Example:
+ *
+ * ```
+ * $user = $users->get(1);
+ * $allArticles = $articles->find('all')->toArray();
+ * $users->Articles->link($user, $allArticles);
+ * ```
+ *
+ * `$user->get('articles')` will contain all articles in `$allArticles` after linking
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
+ * of this association
+ * @param array $targetEntities list of entities belonging to the `target` side
+ * of this association
+ * @param array $options list of options to be passed to the internal `save` call
+ * @return bool true on success, false otherwise
+ */
+ public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool
+ {
+ $saveStrategy = $this->getSaveStrategy();
+ $this->setSaveStrategy(self::SAVE_APPEND);
+ $property = $this->getProperty();
+
+ $currentEntities = array_unique(
+ array_merge(
+ (array)$sourceEntity->get($property),
+ $targetEntities
+ )
+ );
+
+ $sourceEntity->set($property, $currentEntities);
+
+ $savedEntity = $this->getConnection()->transactional(function () use ($sourceEntity, $options) {
+ return $this->saveAssociated($sourceEntity, $options);
+ });
+
+ $ok = ($savedEntity instanceof EntityInterface);
+
+ $this->setSaveStrategy($saveStrategy);
+
+ if ($ok) {
+ $sourceEntity->set($property, $savedEntity->get($property));
+ $sourceEntity->setDirty($property, false);
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Removes all links between the passed source entity and each of the provided
+ * target entities. This method assumes that all passed objects are already persisted
+ * in the database and that each of them contain a primary key value.
+ *
+ * ### Options
+ *
+ * Additionally to the default options accepted by `Table::delete()`, the following
+ * keys are supported:
+ *
+ * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that
+ * are stored in `$sourceEntity` (default: true)
+ *
+ * By default this method will unset each of the entity objects stored inside the
+ * source entity.
+ *
+ * Changes are persisted in the database and also in the source entity.
+ *
+ * ### Example:
+ *
+ * ```
+ * $user = $users->get(1);
+ * $user->articles = [$article1, $article2, $article3, $article4];
+ * $users->save($user, ['Associated' => ['Articles']]);
+ * $allArticles = [$article1, $article2, $article3];
+ * $users->Articles->unlink($user, $allArticles);
+ * ```
+ *
+ * `$article->get('articles')` will contain only `[$article4]` after deleting in the database
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
+ * this association
+ * @param array $targetEntities list of entities persisted in the target table for
+ * this association
+ * @param array|bool $options list of options to be passed to the internal `delete` call.
+ * If boolean it will be used a value for "cleanProperty" option.
+ * @throws \InvalidArgumentException if non persisted entities are passed or if
+ * any of them is lacking a primary key value
+ * @return void
+ */
+ public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = []): void
+ {
+ if (is_bool($options)) {
+ $options = [
+ 'cleanProperty' => $options,
+ ];
+ } else {
+ $options += ['cleanProperty' => true];
+ }
+ if (count($targetEntities) === 0) {
+ return;
+ }
+
+ $foreignKey = (array)$this->getForeignKey();
+ $target = $this->getTarget();
+ $targetPrimaryKey = array_merge((array)$target->getPrimaryKey(), $foreignKey);
+ $property = $this->getProperty();
+
+ $conditions = [
+ 'OR' => (new Collection($targetEntities))
+ ->map(function ($entity) use ($targetPrimaryKey) {
+ /** @var \Cake\Datasource\EntityInterface $entity */
+ return $entity->extract($targetPrimaryKey);
+ })
+ ->toList(),
+ ];
+
+ $this->_unlink($foreignKey, $target, $conditions, $options);
+
+ $result = $sourceEntity->get($property);
+ if ($options['cleanProperty'] && $result !== null) {
+ $sourceEntity->set(
+ $property,
+ (new Collection($sourceEntity->get($property)))
+ ->reject(
+ function ($assoc) use ($targetEntities) {
+ return in_array($assoc, $targetEntities);
+ }
+ )
+ ->toList()
+ );
+ }
+
+ $sourceEntity->setDirty($property, false);
+ }
+
+ /**
+ * Replaces existing association links between the source entity and the target
+ * with the ones passed. This method does a smart cleanup, links that are already
+ * persisted and present in `$targetEntities` will not be deleted, new links will
+ * be created for the passed target entities that are not already in the database
+ * and the rest will be removed.
+ *
+ * For example, if an author has many articles, such as 'article1','article 2' and 'article 3' and you pass
+ * to this method an array containing the entities for articles 'article 1' and 'article 4',
+ * only the link for 'article 1' will be kept in database, the links for 'article 2' and 'article 3' will be
+ * deleted and the link for 'article 4' will be created.
+ *
+ * Existing links are not deleted and created again, they are either left untouched
+ * or updated.
+ *
+ * This method does not check link uniqueness.
+ *
+ * On success, the passed `$sourceEntity` will contain `$targetEntities` as value
+ * in the corresponding property for this association.
+ *
+ * Additional options for new links to be saved can be passed in the third argument,
+ * check `Table::save()` for information on the accepted options.
+ *
+ * ### Example:
+ *
+ * ```
+ * $author->articles = [$article1, $article2, $article3, $article4];
+ * $authors->save($author);
+ * $articles = [$article1, $article3];
+ * $authors->getAssociation('articles')->replace($author, $articles);
+ * ```
+ *
+ * `$author->get('articles')` will contain only `[$article1, $article3]` at the end
+ *
+ * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
+ * this association
+ * @param array $targetEntities list of entities from the target table to be linked
+ * @param array $options list of options to be passed to the internal `save`/`delete` calls
+ * when persisting/updating new links, or deleting existing ones
+ * @throws \InvalidArgumentException if non persisted entities are passed or if
+ * any of them is lacking a primary key value
+ * @return bool success
+ */
+ public function replace(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool
+ {
+ $property = $this->getProperty();
+ $sourceEntity->set($property, $targetEntities);
+ $saveStrategy = $this->getSaveStrategy();
+ $this->setSaveStrategy(self::SAVE_REPLACE);
+ $result = $this->saveAssociated($sourceEntity, $options);
+ $ok = ($result instanceof EntityInterface);
+
+ if ($ok) {
+ $sourceEntity = $result;
+ }
+ $this->setSaveStrategy($saveStrategy);
+
+ return $ok;
+ }
+
+ /**
+ * Deletes/sets null the related objects according to the dependency between source and targets
+ * and foreign key nullability. Skips deleting records present in $remainingEntities
+ *
+ * @param array $foreignKeyReference The foreign key reference defining the link between the
+ * target entity, and the parent entity.
+ * @param \Cake\Datasource\EntityInterface $entity the entity which should have its associated entities unassigned
+ * @param \Cake\ORM\Table $target The associated table
+ * @param iterable $remainingEntities Entities that should not be deleted
+ * @param array $options list of options accepted by `Table::delete()`
+ * @return bool success
+ */
+ protected function _unlinkAssociated(
+ array $foreignKeyReference,
+ EntityInterface $entity,
+ Table $target,
+ iterable $remainingEntities = [],
+ array $options = []
+ ): bool {
+ $primaryKey = (array)$target->getPrimaryKey();
+ $exclusions = new Collection($remainingEntities);
+ $exclusions = $exclusions->map(
+ function ($ent) use ($primaryKey) {
+ /** @var \Cake\Datasource\EntityInterface $ent */
+ return $ent->extract($primaryKey);
+ }
+ )
+ ->filter(
+ function ($v) {
+ return !in_array(null, $v, true);
+ }
+ )
+ ->toList();
+
+ $conditions = $foreignKeyReference;
+
+ if (count($exclusions) > 0) {
+ $conditions = [
+ 'NOT' => [
+ 'OR' => $exclusions,
+ ],
+ $foreignKeyReference,
+ ];
+ }
+
+ return $this->_unlink(array_keys($foreignKeyReference), $target, $conditions, $options);
+ }
+
+ /**
+ * Deletes/sets null the related objects matching $conditions.
+ *
+ * The action which is taken depends on the dependency between source and
+ * targets and also on foreign key nullability.
+ *
+ * @param array $foreignKey array of foreign key properties
+ * @param \Cake\ORM\Table $target The associated table
+ * @param array $conditions The conditions that specifies what are the objects to be unlinked
+ * @param array $options list of options accepted by `Table::delete()`
+ * @return bool success
+ */
+ protected function _unlink(array $foreignKey, Table $target, array $conditions = [], array $options = []): bool
+ {
+ $mustBeDependent = (!$this->_foreignKeyAcceptsNull($target, $foreignKey) || $this->getDependent());
+
+ if ($mustBeDependent) {
+ if ($this->_cascadeCallbacks) {
+ $conditions = new QueryExpression($conditions);
+ $conditions->traverse(function ($entry) use ($target): void {
+ if ($entry instanceof FieldInterface) {
+ $field = $entry->getField();
+ if (is_string($field)) {
+ $entry->setField($target->aliasField($field));
+ }
+ }
+ });
+ $query = $this->find()->where($conditions);
+ $ok = true;
+ foreach ($query as $assoc) {
+ $ok = $ok && $target->delete($assoc, $options);
+ }
+
+ return $ok;
+ }
+
+ $this->deleteAll($conditions);
+
+ return true;
+ }
+
+ $updateFields = array_fill_keys($foreignKey, null);
+ $this->updateAll($updateFields, $conditions);
+
+ return true;
+ }
+
+ /**
+ * Checks the nullable flag of the foreign key
+ *
+ * @param \Cake\ORM\Table $table the table containing the foreign key
+ * @param array $properties the list of fields that compose the foreign key
+ * @return bool
+ */
+ protected function _foreignKeyAcceptsNull(Table $table, array $properties): bool
+ {
+ return !in_array(
+ false,
+ array_map(
+ function ($prop) use ($table) {
+ return $table->getSchema()->isNullable($prop);
+ },
+ $properties
+ )
+ );
+ }
+
+ /**
+ * Get the relationship type.
+ *
+ * @return string
+ */
+ public function type(): string
+ {
+ return self::ONE_TO_MANY;
+ }
+
+ /**
+ * Whether this association can be expressed directly in a query join
+ *
+ * @param array $options custom options key that could alter the return value
+ * @return bool if the 'matching' key in $option is true then this function
+ * will return true, false otherwise
+ */
+ public function canBeJoined(array $options = []): bool
+ {
+ return !empty($options['matching']);
+ }
+
+ /**
+ * Gets the name of the field representing the foreign key to the source table.
+ *
+ * @return string|string[]
+ */
+ public function getForeignKey()
+ {
+ if ($this->_foreignKey === null) {
+ $this->_foreignKey = $this->_modelKey($this->getSource()->getTable());
+ }
+
+ return $this->_foreignKey;
+ }
+
+ /**
+ * Sets the sort order in which target records should be returned.
+ *
+ * @param mixed $sort A find() compatible order clause
+ * @return $this
+ */
+ public function setSort($sort)
+ {
+ $this->_sort = $sort;
+
+ return $this;
+ }
+
+ /**
+ * Gets the sort order in which target records should be returned.
+ *
+ * @return mixed
+ */
+ public function getSort()
+ {
+ return $this->_sort;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function defaultRowValue(array $row, bool $joined): array
+ {
+ $sourceAlias = $this->getSource()->getAlias();
+ if (isset($row[$sourceAlias])) {
+ $row[$sourceAlias][$this->getProperty()] = $joined ? null : [];
+ }
+
+ return $row;
+ }
+
+ /**
+ * Parse extra options passed in the constructor.
+ *
+ * @param array $options original list of options passed in constructor
+ * @return void
+ */
+ protected function _options(array $options): void
+ {
+ if (!empty($options['saveStrategy'])) {
+ $this->setSaveStrategy($options['saveStrategy']);
+ }
+ if (isset($options['sort'])) {
+ $this->setSort($options['sort']);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function eagerLoader(array $options): Closure
+ {
+ $loader = new SelectLoader([
+ 'alias' => $this->getAlias(),
+ 'sourceAlias' => $this->getSource()->getAlias(),
+ 'targetAlias' => $this->getTarget()->getAlias(),
+ 'foreignKey' => $this->getForeignKey(),
+ 'bindingKey' => $this->getBindingKey(),
+ 'strategy' => $this->getStrategy(),
+ 'associationType' => $this->type(),
+ 'sort' => $this->getSort(),
+ 'finder' => [$this, 'find'],
+ ]);
+
+ return $loader->buildEagerLoader($options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function cascadeDelete(EntityInterface $entity, array $options = []): bool
+ {
+ $helper = new DependentDeleteHelper();
+
+ return $helper->cascadeDelete($this, $entity, $options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php b/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php
new file mode 100644
index 000000000..bd10694b7
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/HasOne.php
@@ -0,0 +1,155 @@
+_foreignKey === null) {
+ $this->_foreignKey = $this->_modelKey($this->getSource()->getAlias());
+ }
+
+ return $this->_foreignKey;
+ }
+
+ /**
+ * Returns default property name based on association name.
+ *
+ * @return string
+ */
+ protected function _propertyName(): string
+ {
+ [, $name] = pluginSplit($this->_name);
+
+ return Inflector::underscore(Inflector::singularize($name));
+ }
+
+ /**
+ * Returns whether or not the passed table is the owning side for this
+ * association. This means that rows in the 'target' table would miss important
+ * or required information if the row in 'source' did not exist.
+ *
+ * @param \Cake\ORM\Table $side The potential Table with ownership
+ * @return bool
+ */
+ public function isOwningSide(Table $side): bool
+ {
+ return $side === $this->getSource();
+ }
+
+ /**
+ * Get the relationship type.
+ *
+ * @return string
+ */
+ public function type(): string
+ {
+ return self::ONE_TO_ONE;
+ }
+
+ /**
+ * Takes an entity from the source table and looks if there is a field
+ * matching the property name for this association. The found entity will be
+ * saved on the target table for this association by passing supplied
+ * `$options`
+ *
+ * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
+ * @param array $options options to be passed to the save method in the target table
+ * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns
+ * the saved entity
+ * @see \Cake\ORM\Table::save()
+ */
+ public function saveAssociated(EntityInterface $entity, array $options = [])
+ {
+ $targetEntity = $entity->get($this->getProperty());
+ if (empty($targetEntity) || !($targetEntity instanceof EntityInterface)) {
+ return $entity;
+ }
+
+ $properties = array_combine(
+ (array)$this->getForeignKey(),
+ $entity->extract((array)$this->getBindingKey())
+ );
+ $targetEntity->set($properties, ['guard' => false]);
+
+ if (!$this->getTarget()->save($targetEntity, $options)) {
+ $targetEntity->unset(array_keys($properties));
+
+ return false;
+ }
+
+ return $entity;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function eagerLoader(array $options): Closure
+ {
+ $loader = new SelectLoader([
+ 'alias' => $this->getAlias(),
+ 'sourceAlias' => $this->getSource()->getAlias(),
+ 'targetAlias' => $this->getTarget()->getAlias(),
+ 'foreignKey' => $this->getForeignKey(),
+ 'bindingKey' => $this->getBindingKey(),
+ 'strategy' => $this->getStrategy(),
+ 'associationType' => $this->type(),
+ 'finder' => [$this, 'find'],
+ ]);
+
+ return $loader->buildEagerLoader($options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function cascadeDelete(EntityInterface $entity, array $options = []): bool
+ {
+ $helper = new DependentDeleteHelper();
+
+ return $helper->cascadeDelete($this, $entity, $options);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.php b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.php
new file mode 100644
index 000000000..f3be34f52
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.php
@@ -0,0 +1,549 @@
+alias = $options['alias'];
+ $this->sourceAlias = $options['sourceAlias'];
+ $this->targetAlias = $options['targetAlias'];
+ $this->foreignKey = $options['foreignKey'];
+ $this->strategy = $options['strategy'];
+ $this->bindingKey = $options['bindingKey'];
+ $this->finder = $options['finder'];
+ $this->associationType = $options['associationType'];
+ $this->sort = $options['sort'] ?? null;
+ }
+
+ /**
+ * Returns a callable that can be used for injecting association results into a given
+ * iterator. The options accepted by this method are the same as `Association::eagerLoader()`
+ *
+ * @param array $options Same options as `Association::eagerLoader()`
+ * @return \Closure
+ */
+ public function buildEagerLoader(array $options): Closure
+ {
+ $options += $this->_defaultOptions();
+ $fetchQuery = $this->_buildQuery($options);
+ $resultMap = $this->_buildResultMap($fetchQuery, $options);
+
+ return $this->_resultInjector($fetchQuery, $resultMap, $options);
+ }
+
+ /**
+ * Returns the default options to use for the eagerLoader
+ *
+ * @return array
+ */
+ protected function _defaultOptions(): array
+ {
+ return [
+ 'foreignKey' => $this->foreignKey,
+ 'conditions' => [],
+ 'strategy' => $this->strategy,
+ 'nestKey' => $this->alias,
+ 'sort' => $this->sort,
+ ];
+ }
+
+ /**
+ * Auxiliary function to construct a new Query object to return all the records
+ * in the target table that are associated to those specified in $options from
+ * the source table
+ *
+ * @param array $options options accepted by eagerLoader()
+ * @return \Cake\ORM\Query
+ * @throws \InvalidArgumentException When a key is required for associations but not selected.
+ */
+ protected function _buildQuery(array $options): Query
+ {
+ $key = $this->_linkField($options);
+ $filter = $options['keys'];
+ $useSubquery = $options['strategy'] === Association::STRATEGY_SUBQUERY;
+ $finder = $this->finder;
+
+ if (!isset($options['fields'])) {
+ $options['fields'] = [];
+ }
+
+ /** @var \Cake\ORM\Query $query */
+ $query = $finder();
+ if (isset($options['finder'])) {
+ [$finderName, $opts] = $this->_extractFinder($options['finder']);
+ $query = $query->find($finderName, $opts);
+ }
+
+ $fetchQuery = $query
+ ->select($options['fields'])
+ ->where($options['conditions'])
+ ->eagerLoaded(true)
+ ->enableHydration($options['query']->isHydrationEnabled());
+
+ if ($useSubquery) {
+ $filter = $this->_buildSubquery($options['query']);
+ $fetchQuery = $this->_addFilteringJoin($fetchQuery, $key, $filter);
+ } else {
+ $fetchQuery = $this->_addFilteringCondition($fetchQuery, $key, $filter);
+ }
+
+ if (!empty($options['sort'])) {
+ $fetchQuery->order($options['sort']);
+ }
+
+ if (!empty($options['contain'])) {
+ $fetchQuery->contain($options['contain']);
+ }
+
+ if (!empty($options['queryBuilder'])) {
+ $fetchQuery = $options['queryBuilder']($fetchQuery);
+ }
+
+ $this->_assertFieldsPresent($fetchQuery, (array)$key);
+
+ return $fetchQuery;
+ }
+
+ /**
+ * Helper method to infer the requested finder and its options.
+ *
+ * Returns the inferred options from the finder $type.
+ *
+ * ### Examples:
+ *
+ * The following will call the finder 'translations' with the value of the finder as its options:
+ * $query->contain(['Comments' => ['finder' => ['translations']]]);
+ * $query->contain(['Comments' => ['finder' => ['translations' => []]]]);
+ * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]);
+ *
+ * @param string|array $finderData The finder name or an array having the name as key
+ * and options as value.
+ * @return array
+ */
+ protected function _extractFinder($finderData): array
+ {
+ $finderData = (array)$finderData;
+
+ if (is_numeric(key($finderData))) {
+ return [current($finderData), []];
+ }
+
+ return [key($finderData), current($finderData)];
+ }
+
+ /**
+ * Checks that the fetching query either has auto fields on or
+ * has the foreignKey fields selected.
+ * If the required fields are missing, throws an exception.
+ *
+ * @param \Cake\ORM\Query $fetchQuery The association fetching query
+ * @param array $key The foreign key fields to check
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ protected function _assertFieldsPresent(Query $fetchQuery, array $key): void
+ {
+ $select = $fetchQuery->aliasFields($fetchQuery->clause('select'));
+ if (empty($select)) {
+ return;
+ }
+ $missingKey = function ($fieldList, $key) {
+ foreach ($key as $keyField) {
+ if (!in_array($keyField, $fieldList, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ $missingFields = $missingKey($select, $key);
+ if ($missingFields) {
+ $driver = $fetchQuery->getConnection()->getDriver();
+ $quoted = array_map([$driver, 'quoteIdentifier'], $key);
+ $missingFields = $missingKey($select, $quoted);
+ }
+
+ if ($missingFields) {
+ throw new InvalidArgumentException(
+ sprintf(
+ 'You are required to select the "%s" field(s)',
+ implode(', ', $key)
+ )
+ );
+ }
+ }
+
+ /**
+ * Appends any conditions required to load the relevant set of records in the
+ * target table query given a filter key and some filtering values when the
+ * filtering needs to be done using a subquery.
+ *
+ * @param \Cake\ORM\Query $query Target table's query
+ * @param string|string[] $key the fields that should be used for filtering
+ * @param \Cake\ORM\Query $subquery The Subquery to use for filtering
+ * @return \Cake\ORM\Query
+ */
+ protected function _addFilteringJoin(Query $query, $key, $subquery): Query
+ {
+ $filter = [];
+ $aliasedTable = $this->sourceAlias;
+
+ foreach ($subquery->clause('select') as $aliasedField => $field) {
+ if (is_int($aliasedField)) {
+ $filter[] = new IdentifierExpression($field);
+ } else {
+ $filter[$aliasedField] = $field;
+ }
+ }
+ $subquery->select($filter, true);
+
+ if (is_array($key)) {
+ $conditions = $this->_createTupleCondition($query, $key, $filter, '=');
+ } else {
+ $filter = current($filter);
+ $conditions = $query->newExpr([$key => $filter]);
+ }
+
+ return $query->innerJoin(
+ [$aliasedTable => $subquery],
+ $conditions
+ );
+ }
+
+ /**
+ * Appends any conditions required to load the relevant set of records in the
+ * target table query given a filter key and some filtering values.
+ *
+ * @param \Cake\ORM\Query $query Target table's query
+ * @param string|array $key The fields that should be used for filtering
+ * @param mixed $filter The value that should be used to match for $key
+ * @return \Cake\ORM\Query
+ */
+ protected function _addFilteringCondition(Query $query, $key, $filter): Query
+ {
+ if (is_array($key)) {
+ $conditions = $this->_createTupleCondition($query, $key, $filter, 'IN');
+ } else {
+ $conditions = [$key . ' IN' => $filter];
+ }
+
+ return $query->andWhere($conditions);
+ }
+
+ /**
+ * Returns a TupleComparison object that can be used for matching all the fields
+ * from $keys with the tuple values in $filter using the provided operator.
+ *
+ * @param \Cake\ORM\Query $query Target table's query
+ * @param string[] $keys the fields that should be used for filtering
+ * @param mixed $filter the value that should be used to match for $key
+ * @param string $operator The operator for comparing the tuples
+ * @return \Cake\Database\Expression\TupleComparison
+ */
+ protected function _createTupleCondition(Query $query, array $keys, $filter, $operator): TupleComparison
+ {
+ $types = [];
+ $defaults = $query->getDefaultTypes();
+ foreach ($keys as $k) {
+ if (isset($defaults[$k])) {
+ $types[] = $defaults[$k];
+ }
+ }
+
+ return new TupleComparison($keys, $filter, $types, $operator);
+ }
+
+ /**
+ * Generates a string used as a table field that contains the values upon
+ * which the filter should be applied
+ *
+ * @param array $options The options for getting the link field.
+ * @return string|string[]
+ * @throws \RuntimeException
+ */
+ protected function _linkField(array $options)
+ {
+ $links = [];
+ $name = $this->alias;
+
+ if ($options['foreignKey'] === false && $this->associationType === Association::ONE_TO_MANY) {
+ $msg = 'Cannot have foreignKey = false for hasMany associations. ' .
+ 'You must provide a foreignKey column.';
+ throw new RuntimeException($msg);
+ }
+
+ $keys = in_array($this->associationType, [Association::ONE_TO_ONE, Association::ONE_TO_MANY], true) ?
+ $this->foreignKey :
+ $this->bindingKey;
+
+ foreach ((array)$keys as $key) {
+ $links[] = sprintf('%s.%s', $name, $key);
+ }
+
+ if (count($links) === 1) {
+ return $links[0];
+ }
+
+ return $links;
+ }
+
+ /**
+ * Builds a query to be used as a condition for filtering records in the
+ * target table, it is constructed by cloning the original query that was used
+ * to load records in the source table.
+ *
+ * @param \Cake\ORM\Query $query the original query used to load source records
+ * @return \Cake\ORM\Query
+ */
+ protected function _buildSubquery(Query $query): Query
+ {
+ $filterQuery = clone $query;
+ $filterQuery->disableAutoFields();
+ $filterQuery->mapReduce(null, null, true);
+ $filterQuery->formatResults(null, true);
+ $filterQuery->contain([], true);
+ $filterQuery->setValueBinder(new ValueBinder());
+
+ if (!$filterQuery->clause('limit')) {
+ $filterQuery->limit(null);
+ $filterQuery->order([], true);
+ $filterQuery->offset(null);
+ }
+
+ $fields = $this->_subqueryFields($query);
+ $filterQuery->select($fields['select'], true)->group($fields['group']);
+
+ return $filterQuery;
+ }
+
+ /**
+ * Calculate the fields that need to participate in a subquery.
+ *
+ * Normally this includes the binding key columns. If there is a an ORDER BY,
+ * those columns are also included as the fields may be calculated or constant values,
+ * that need to be present to ensure the correct association data is loaded.
+ *
+ * @param \Cake\ORM\Query $query The query to get fields from.
+ * @return array The list of fields for the subquery.
+ */
+ protected function _subqueryFields(Query $query): array
+ {
+ $keys = (array)$this->bindingKey;
+
+ if ($this->associationType === Association::MANY_TO_ONE) {
+ $keys = (array)$this->foreignKey;
+ }
+
+ $fields = $query->aliasFields($keys, $this->sourceAlias);
+ $group = $fields = array_values($fields);
+
+ $order = $query->clause('order');
+ if ($order) {
+ $columns = $query->clause('select');
+ $order->iterateParts(function ($direction, $field) use (&$fields, $columns): void {
+ if (isset($columns[$field])) {
+ $fields[$field] = $columns[$field];
+ }
+ });
+ }
+
+ return ['select' => $fields, 'group' => $group];
+ }
+
+ /**
+ * Builds an array containing the results from fetchQuery indexed by
+ * the foreignKey value corresponding to this association.
+ *
+ * @param \Cake\ORM\Query $fetchQuery The query to get results from
+ * @param array $options The options passed to the eager loader
+ * @return array
+ */
+ protected function _buildResultMap(Query $fetchQuery, array $options): array
+ {
+ $resultMap = [];
+ $singleResult = in_array($this->associationType, [Association::MANY_TO_ONE, Association::ONE_TO_ONE], true);
+ $keys = in_array($this->associationType, [Association::ONE_TO_ONE, Association::ONE_TO_MANY], true) ?
+ $this->foreignKey :
+ $this->bindingKey;
+ $key = (array)$keys;
+
+ foreach ($fetchQuery->all() as $result) {
+ $values = [];
+ foreach ($key as $k) {
+ $values[] = $result[$k];
+ }
+ if ($singleResult) {
+ $resultMap[implode(';', $values)] = $result;
+ } else {
+ $resultMap[implode(';', $values)][] = $result;
+ }
+ }
+
+ return $resultMap;
+ }
+
+ /**
+ * Returns a callable to be used for each row in a query result set
+ * for injecting the eager loaded rows
+ *
+ * @param \Cake\ORM\Query $fetchQuery the Query used to fetch results
+ * @param array $resultMap an array with the foreignKey as keys and
+ * the corresponding target table results as value.
+ * @param array $options The options passed to the eagerLoader method
+ * @return \Closure
+ */
+ protected function _resultInjector(Query $fetchQuery, array $resultMap, array $options): Closure
+ {
+ $keys = $this->associationType === Association::MANY_TO_ONE ?
+ $this->foreignKey :
+ $this->bindingKey;
+
+ $sourceKeys = [];
+ foreach ((array)$keys as $key) {
+ $f = $fetchQuery->aliasField($key, $this->sourceAlias);
+ $sourceKeys[] = key($f);
+ }
+
+ $nestKey = $options['nestKey'];
+ if (count($sourceKeys) > 1) {
+ return $this->_multiKeysInjector($resultMap, $sourceKeys, $nestKey);
+ }
+
+ $sourceKey = $sourceKeys[0];
+
+ return function ($row) use ($resultMap, $sourceKey, $nestKey) {
+ if (isset($row[$sourceKey], $resultMap[$row[$sourceKey]])) {
+ $row[$nestKey] = $resultMap[$row[$sourceKey]];
+ }
+
+ return $row;
+ };
+ }
+
+ /**
+ * Returns a callable to be used for each row in a query result set
+ * for injecting the eager loaded rows when the matching needs to
+ * be done with multiple foreign keys
+ *
+ * @param array $resultMap A keyed arrays containing the target table
+ * @param string[] $sourceKeys An array with aliased keys to match
+ * @param string $nestKey The key under which results should be nested
+ * @return \Closure
+ */
+ protected function _multiKeysInjector(array $resultMap, array $sourceKeys, string $nestKey): Closure
+ {
+ return function ($row) use ($resultMap, $sourceKeys, $nestKey) {
+ $values = [];
+ foreach ($sourceKeys as $key) {
+ $values[] = $row[$key];
+ }
+
+ $key = implode(';', $values);
+ if (isset($resultMap[$key])) {
+ $row[$nestKey] = $resultMap[$key];
+ }
+
+ return $row;
+ };
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectWithPivotLoader.php b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectWithPivotLoader.php
new file mode 100644
index 000000000..128f52f51
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectWithPivotLoader.php
@@ -0,0 +1,194 @@
+junctionAssociationName = $options['junctionAssociationName'];
+ $this->junctionProperty = $options['junctionProperty'];
+ $this->junctionAssoc = $options['junctionAssoc'];
+ $this->junctionConditions = $options['junctionConditions'];
+ }
+
+ /**
+ * Auxiliary function to construct a new Query object to return all the records
+ * in the target table that are associated to those specified in $options from
+ * the source table.
+ *
+ * This is used for eager loading records on the target table based on conditions.
+ *
+ * @param array $options options accepted by eagerLoader()
+ * @return \Cake\ORM\Query
+ * @throws \InvalidArgumentException When a key is required for associations but not selected.
+ */
+ protected function _buildQuery(array $options): Query
+ {
+ $name = $this->junctionAssociationName;
+ $assoc = $this->junctionAssoc;
+ $queryBuilder = false;
+
+ if (!empty($options['queryBuilder'])) {
+ $queryBuilder = $options['queryBuilder'];
+ unset($options['queryBuilder']);
+ }
+
+ $query = parent::_buildQuery($options);
+
+ if ($queryBuilder) {
+ $query = $queryBuilder($query);
+ }
+
+ if ($query->isAutoFieldsEnabled() === null) {
+ $query->enableAutoFields($query->clause('select') === []);
+ }
+
+ // Ensure that association conditions are applied
+ // and that the required keys are in the selected columns.
+
+ $tempName = $this->alias . '_CJoin';
+ $schema = $assoc->getSchema();
+ $joinFields = $types = [];
+
+ foreach ($schema->typeMap() as $f => $type) {
+ $key = $tempName . '__' . $f;
+ $joinFields[$key] = "$name.$f";
+ $types[$key] = $type;
+ }
+
+ $query
+ ->where($this->junctionConditions)
+ ->select($joinFields);
+
+ $query
+ ->getEagerLoader()
+ ->addToJoinsMap($tempName, $assoc, false, $this->junctionProperty);
+
+ $assoc->attachTo($query, [
+ 'aliasPath' => $assoc->getAlias(),
+ 'includeFields' => false,
+ 'propertyPath' => $this->junctionProperty,
+ ]);
+ $query->getTypeMap()->addDefaults($types);
+
+ return $query;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _assertFieldsPresent(Query $fetchQuery, array $key): void
+ {
+ // _buildQuery() manually adds in required fields from junction table
+ }
+
+ /**
+ * Generates a string used as a table field that contains the values upon
+ * which the filter should be applied
+ *
+ * @param array $options the options to use for getting the link field.
+ * @return string|string[]
+ */
+ protected function _linkField(array $options)
+ {
+ $links = [];
+ $name = $this->junctionAssociationName;
+
+ foreach ((array)$options['foreignKey'] as $key) {
+ $links[] = sprintf('%s.%s', $name, $key);
+ }
+
+ if (count($links) === 1) {
+ return $links[0];
+ }
+
+ return $links;
+ }
+
+ /**
+ * Builds an array containing the results from fetchQuery indexed by
+ * the foreignKey value corresponding to this association.
+ *
+ * @param \Cake\ORM\Query $fetchQuery The query to get results from
+ * @param array $options The options passed to the eager loader
+ * @return array
+ * @throws \RuntimeException when the association property is not part of the results set.
+ */
+ protected function _buildResultMap(Query $fetchQuery, array $options): array
+ {
+ $resultMap = [];
+ $key = (array)$options['foreignKey'];
+
+ foreach ($fetchQuery->all() as $result) {
+ if (!isset($result[$this->junctionProperty])) {
+ throw new RuntimeException(sprintf(
+ '"%s" is missing from the belongsToMany results. Results cannot be created.',
+ $this->junctionProperty
+ ));
+ }
+
+ $values = [];
+ foreach ($key as $k) {
+ $values[] = $result[$this->junctionProperty][$k];
+ }
+ $resultMap[implode(';', $values)][] = $result;
+ }
+
+ return $resultMap;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/AssociationCollection.php b/app/vendor/cakephp/cakephp/src/ORM/AssociationCollection.php
new file mode 100644
index 000000000..6a025dd35
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/AssociationCollection.php
@@ -0,0 +1,383 @@
+_tableLocator = $tableLocator;
+ }
+ }
+
+ /**
+ * Add an association to the collection
+ *
+ * If the alias added contains a `.` the part preceding the `.` will be dropped.
+ * This makes using plugins simpler as the Plugin.Class syntax is frequently used.
+ *
+ * @param string $alias The association alias
+ * @param \Cake\ORM\Association $association The association to add.
+ * @return \Cake\ORM\Association The association object being added.
+ */
+ public function add(string $alias, Association $association): Association
+ {
+ [, $alias] = pluginSplit($alias);
+
+ return $this->_items[$alias] = $association;
+ }
+
+ /**
+ * Creates and adds the Association object to this collection.
+ *
+ * @param string $className The name of association class.
+ * @param string $associated The alias for the target table.
+ * @param array $options List of options to configure the association definition.
+ * @return \Cake\ORM\Association
+ * @throws \InvalidArgumentException
+ */
+ public function load(string $className, string $associated, array $options = []): Association
+ {
+ $options += [
+ 'tableLocator' => $this->getTableLocator(),
+ ];
+
+ $association = new $className($associated, $options);
+ if (!$association instanceof Association) {
+ $message = sprintf(
+ 'The association must extend `%s` class, `%s` given.',
+ Association::class,
+ get_class($association)
+ );
+ throw new InvalidArgumentException($message);
+ }
+
+ return $this->add($association->getName(), $association);
+ }
+
+ /**
+ * Fetch an attached association by name.
+ *
+ * @param string $alias The association alias to get.
+ * @return \Cake\ORM\Association|null Either the association or null.
+ */
+ public function get(string $alias): ?Association
+ {
+ if (isset($this->_items[$alias])) {
+ return $this->_items[$alias];
+ }
+
+ return null;
+ }
+
+ /**
+ * Fetch an association by property name.
+ *
+ * @param string $prop The property to find an association by.
+ * @return \Cake\ORM\Association|null Either the association or null.
+ */
+ public function getByProperty(string $prop): ?Association
+ {
+ foreach ($this->_items as $assoc) {
+ if ($assoc->getProperty() === $prop) {
+ return $assoc;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check for an attached association by name.
+ *
+ * @param string $alias The association alias to get.
+ * @return bool Whether or not the association exists.
+ */
+ public function has(string $alias): bool
+ {
+ return isset($this->_items[$alias]);
+ }
+
+ /**
+ * Get the names of all the associations in the collection.
+ *
+ * @return string[]
+ */
+ public function keys(): array
+ {
+ return array_keys($this->_items);
+ }
+
+ /**
+ * Get an array of associations matching a specific type.
+ *
+ * @param string|array $class The type of associations you want.
+ * For example 'BelongsTo' or array like ['BelongsTo', 'HasOne']
+ * @return \Cake\ORM\Association[] An array of Association objects.
+ * @since 3.5.3
+ */
+ public function getByType($class): array
+ {
+ $class = array_map('strtolower', (array)$class);
+
+ $out = array_filter($this->_items, function ($assoc) use ($class) {
+ [, $name] = namespaceSplit(get_class($assoc));
+
+ return in_array(strtolower($name), $class, true);
+ });
+
+ return array_values($out);
+ }
+
+ /**
+ * Drop/remove an association.
+ *
+ * Once removed the association will not longer be reachable
+ *
+ * @param string $alias The alias name.
+ * @return void
+ */
+ public function remove(string $alias): void
+ {
+ unset($this->_items[$alias]);
+ }
+
+ /**
+ * Remove all registered associations.
+ *
+ * Once removed associations will not longer be reachable
+ *
+ * @return void
+ */
+ public function removeAll(): void
+ {
+ foreach ($this->_items as $alias => $object) {
+ $this->remove($alias);
+ }
+ }
+
+ /**
+ * Save all the associations that are parents of the given entity.
+ *
+ * Parent associations include any association where the given table
+ * is the owning side.
+ *
+ * @param \Cake\ORM\Table $table The table entity is for.
+ * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for.
+ * @param array $associations The list of associations to save parents from.
+ * associations not in this list will not be saved.
+ * @param array $options The options for the save operation.
+ * @return bool Success
+ */
+ public function saveParents(Table $table, EntityInterface $entity, array $associations, array $options = []): bool
+ {
+ if (empty($associations)) {
+ return true;
+ }
+
+ return $this->_saveAssociations($table, $entity, $associations, $options, false);
+ }
+
+ /**
+ * Save all the associations that are children of the given entity.
+ *
+ * Child associations include any association where the given table
+ * is not the owning side.
+ *
+ * @param \Cake\ORM\Table $table The table entity is for.
+ * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for.
+ * @param array $associations The list of associations to save children from.
+ * associations not in this list will not be saved.
+ * @param array $options The options for the save operation.
+ * @return bool Success
+ */
+ public function saveChildren(Table $table, EntityInterface $entity, array $associations, array $options): bool
+ {
+ if (empty($associations)) {
+ return true;
+ }
+
+ return $this->_saveAssociations($table, $entity, $associations, $options, true);
+ }
+
+ /**
+ * Helper method for saving an association's data.
+ *
+ * @param \Cake\ORM\Table $table The table the save is currently operating on
+ * @param \Cake\Datasource\EntityInterface $entity The entity to save
+ * @param array $associations Array of associations to save.
+ * @param array $options Original options
+ * @param bool $owningSide Compared with association classes'
+ * isOwningSide method.
+ * @return bool Success
+ * @throws \InvalidArgumentException When an unknown alias is used.
+ */
+ protected function _saveAssociations(
+ Table $table,
+ EntityInterface $entity,
+ array $associations,
+ array $options,
+ bool $owningSide
+ ): bool {
+ unset($options['associated']);
+ foreach ($associations as $alias => $nested) {
+ if (is_int($alias)) {
+ $alias = $nested;
+ $nested = [];
+ }
+ $relation = $this->get($alias);
+ if (!$relation) {
+ $msg = sprintf(
+ 'Cannot save %s, it is not associated to %s',
+ $alias,
+ $table->getAlias()
+ );
+ throw new InvalidArgumentException($msg);
+ }
+ if ($relation->isOwningSide($table) !== $owningSide) {
+ continue;
+ }
+ if (!$this->_save($relation, $entity, $nested, $options)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Helper method for saving an association's data.
+ *
+ * @param \Cake\ORM\Association $association The association object to save with.
+ * @param \Cake\Datasource\EntityInterface $entity The entity to save
+ * @param array $nested Options for deeper associations
+ * @param array $options Original options
+ * @return bool Success
+ */
+ protected function _save(
+ Association $association,
+ EntityInterface $entity,
+ array $nested,
+ array $options
+ ): bool {
+ if (!$entity->isDirty($association->getProperty())) {
+ return true;
+ }
+ if (!empty($nested)) {
+ $options = $nested + $options;
+ }
+
+ return (bool)$association->saveAssociated($entity, $options);
+ }
+
+ /**
+ * Cascade a delete across the various associations.
+ * Cascade first across associations for which cascadeCallbacks is true.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to delete associations for.
+ * @param array $options The options used in the delete operation.
+ * @return bool
+ */
+ public function cascadeDelete(EntityInterface $entity, array $options): bool
+ {
+ $noCascade = [];
+ foreach ($this->_items as $assoc) {
+ if (!$assoc->getCascadeCallbacks()) {
+ $noCascade[] = $assoc;
+ continue;
+ }
+ $success = $assoc->cascadeDelete($entity, $options);
+ if (!$success) {
+ return false;
+ }
+ }
+
+ foreach ($noCascade as $assoc) {
+ $success = $assoc->cascadeDelete($entity, $options);
+ if (!$success) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an associative array of association names out a mixed
+ * array. If true is passed, then it returns all association names
+ * in this collection.
+ *
+ * @param bool|array $keys the list of association names to normalize
+ * @return array
+ */
+ public function normalizeKeys($keys): array
+ {
+ if ($keys === true) {
+ $keys = $this->keys();
+ }
+
+ if (empty($keys)) {
+ return [];
+ }
+
+ return $this->_normalizeAssociations($keys);
+ }
+
+ /**
+ * Allow looping through the associations
+ *
+ * @return \Cake\ORM\Association[]
+ * @psalm-return \Traversable
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->_items);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/AssociationsNormalizerTrait.php b/app/vendor/cakephp/cakephp/src/ORM/AssociationsNormalizerTrait.php
new file mode 100644
index 000000000..5b27a7dbe
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/AssociationsNormalizerTrait.php
@@ -0,0 +1,69 @@
+ $options) {
+ $pointer = &$result;
+
+ if (is_int($table)) {
+ $table = $options;
+ $options = [];
+ }
+
+ if (!strpos($table, '.')) {
+ $result[$table] = $options;
+ continue;
+ }
+
+ $path = explode('.', $table);
+ $table = array_pop($path);
+ /** @var string $first */
+ $first = array_shift($path);
+ $pointer += [$first => []];
+ $pointer = &$pointer[$first];
+ $pointer += ['associated' => []];
+
+ foreach ($path as $t) {
+ $pointer += ['associated' => []];
+ $pointer['associated'] += [$t => []];
+ $pointer['associated'][$t] += ['associated' => []];
+ $pointer = &$pointer['associated'][$t];
+ }
+
+ $pointer['associated'] += [$table => []];
+ $pointer['associated'][$table] = $options + $pointer['associated'][$table];
+ }
+
+ return $result['associated'] ?? $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior.php
new file mode 100644
index 000000000..1f7939ab8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior.php
@@ -0,0 +1,441 @@
+doSomething($arg1, $arg2);`.
+ *
+ * ### Callback methods
+ *
+ * Behaviors can listen to any events fired on a Table. By default
+ * CakePHP provides a number of lifecycle events your behaviors can
+ * listen to:
+ *
+ * - `beforeFind(EventInterface $event, Query $query, ArrayObject $options, boolean $primary)`
+ * Fired before each find operation. By stopping the event and supplying a
+ * return value you can bypass the find operation entirely. Any changes done
+ * to the $query instance will be retained for the rest of the find. The
+ * $primary parameter indicates whether or not this is the root query,
+ * or an associated query.
+ *
+ * - `buildValidator(EventInterface $event, Validator $validator, string $name)`
+ * Fired when the validator object identified by $name is being built. You can use this
+ * callback to add validation rules or add validation providers.
+ *
+ * - `buildRules(EventInterface $event, RulesChecker $rules)`
+ * Fired when the rules checking object for the table is being built. You can use this
+ * callback to add more rules to the set.
+ *
+ * - `beforeRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, $operation)`
+ * Fired before an entity is validated using by a rules checker. By stopping this event,
+ * you can return the final value of the rules checking operation.
+ *
+ * - `afterRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, bool $result, $operation)`
+ * Fired after the rules have been checked on the entity. By stopping this event,
+ * you can return the final value of the rules checking operation.
+ *
+ * - `beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * Fired before each entity is saved. Stopping this event will abort the save
+ * operation. When the event is stopped the result of the event will be returned.
+ *
+ * - `afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * Fired after an entity is saved.
+ *
+ * - `beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * Fired before an entity is deleted. By stopping this event you will abort
+ * the delete operation.
+ *
+ * - `afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * Fired after an entity has been deleted.
+ *
+ * In addition to the core events, behaviors can respond to any
+ * event fired from your Table classes including custom application
+ * specific ones.
+ *
+ * You can set the priority of a behaviors callbacks by using the
+ * `priority` setting when attaching a behavior. This will set the
+ * priority for all the callbacks a behavior provides.
+ *
+ * ### Finder methods
+ *
+ * Behaviors can provide finder methods that hook into a Table's
+ * find() method. Custom finders are a great way to provide preset
+ * queries that relate to your behavior. For example a SluggableBehavior
+ * could provide a find('slugged') finder. Behavior finders
+ * are implemented the same as other finders. Any method
+ * starting with `find` will be setup as a finder. Your finder
+ * methods should expect the following arguments:
+ *
+ * ```
+ * findSlugged(Query $query, array $options)
+ * ```
+ *
+ * @see \Cake\ORM\Table::addBehavior()
+ * @see \Cake\Event\EventManager
+ */
+class Behavior implements EventListenerInterface
+{
+ use InstanceConfigTrait;
+
+ /**
+ * Table instance.
+ *
+ * @var \Cake\ORM\Table
+ */
+ protected $_table;
+
+ /**
+ * Reflection method cache for behaviors.
+ *
+ * Stores the reflected method + finder methods per class.
+ * This prevents reflecting the same class multiple times in a single process.
+ *
+ * @var array
+ */
+ protected static $_reflectionCache = [];
+
+ /**
+ * Default configuration
+ *
+ * These are merged with user-provided configuration when the behavior is used.
+ *
+ * @var array
+ */
+ protected $_defaultConfig = [];
+
+ /**
+ * Constructor
+ *
+ * Merges config with the default and store in the config property
+ *
+ * @param \Cake\ORM\Table $table The table this behavior is attached to.
+ * @param array $config The config for this behavior.
+ */
+ public function __construct(Table $table, array $config = [])
+ {
+ $config = $this->_resolveMethodAliases(
+ 'implementedFinders',
+ $this->_defaultConfig,
+ $config
+ );
+ $config = $this->_resolveMethodAliases(
+ 'implementedMethods',
+ $this->_defaultConfig,
+ $config
+ );
+ $this->_table = $table;
+ $this->setConfig($config);
+ $this->initialize($config);
+ }
+
+ /**
+ * Constructor hook method.
+ *
+ * Implement this method to avoid having to overwrite
+ * the constructor and call parent.
+ *
+ * @param array $config The configuration settings provided to this behavior.
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ }
+
+ /**
+ * Get the table instance this behavior is bound to.
+ *
+ * @return \Cake\ORM\Table The bound table instance.
+ * @deprecated 4.2.0 Use table() instead.
+ */
+ public function getTable(): Table
+ {
+ deprecationWarning('Behavior::getTable() is deprecated. Use table() instead.');
+
+ return $this->table();
+ }
+
+ /**
+ * Get the table instance this behavior is bound to.
+ *
+ * @return \Cake\ORM\Table The bound table instance.
+ */
+ public function table(): Table
+ {
+ return $this->_table;
+ }
+
+ /**
+ * Removes aliased methods that would otherwise be duplicated by userland configuration.
+ *
+ * @param string $key The key to filter.
+ * @param array $defaults The default method mappings.
+ * @param array $config The customized method mappings.
+ * @return array A de-duped list of config data.
+ */
+ protected function _resolveMethodAliases(string $key, array $defaults, array $config): array
+ {
+ if (!isset($defaults[$key], $config[$key])) {
+ return $config;
+ }
+ if (isset($config[$key]) && $config[$key] === []) {
+ $this->setConfig($key, [], false);
+ unset($config[$key]);
+
+ return $config;
+ }
+
+ $indexed = array_flip($defaults[$key]);
+ $indexedCustom = array_flip($config[$key]);
+ foreach ($indexed as $method => $alias) {
+ if (!isset($indexedCustom[$method])) {
+ $indexedCustom[$method] = $alias;
+ }
+ }
+ $this->setConfig($key, array_flip($indexedCustom), false);
+ unset($config[$key]);
+
+ return $config;
+ }
+
+ /**
+ * verifyConfig
+ *
+ * Checks that implemented keys contain values pointing at callable.
+ *
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException if config are invalid
+ */
+ public function verifyConfig(): void
+ {
+ $keys = ['implementedFinders', 'implementedMethods'];
+ foreach ($keys as $key) {
+ if (!isset($this->_config[$key])) {
+ continue;
+ }
+
+ foreach ($this->_config[$key] as $method) {
+ if (!is_callable([$this, $method])) {
+ throw new CakeException(sprintf(
+ 'The method %s is not callable on class %s',
+ $method,
+ static::class
+ ));
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the Model callbacks this behavior is interested in.
+ *
+ * By defining one of the callback methods a behavior is assumed
+ * to be interested in the related event.
+ *
+ * Override this method if you need to add non-conventional event listeners.
+ * Or if you want your behavior to listen to non-standard events.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ $eventMap = [
+ 'Model.beforeMarshal' => 'beforeMarshal',
+ 'Model.afterMarshal' => 'afterMarshal',
+ 'Model.beforeFind' => 'beforeFind',
+ 'Model.beforeSave' => 'beforeSave',
+ 'Model.afterSave' => 'afterSave',
+ 'Model.afterSaveCommit' => 'afterSaveCommit',
+ 'Model.beforeDelete' => 'beforeDelete',
+ 'Model.afterDelete' => 'afterDelete',
+ 'Model.afterDeleteCommit' => 'afterDeleteCommit',
+ 'Model.buildValidator' => 'buildValidator',
+ 'Model.buildRules' => 'buildRules',
+ 'Model.beforeRules' => 'beforeRules',
+ 'Model.afterRules' => 'afterRules',
+ ];
+ $config = $this->getConfig();
+ $priority = $config['priority'] ?? null;
+ $events = [];
+
+ foreach ($eventMap as $event => $method) {
+ if (!method_exists($this, $method)) {
+ continue;
+ }
+ if ($priority === null) {
+ $events[$event] = $method;
+ } else {
+ $events[$event] = [
+ 'callable' => $method,
+ 'priority' => $priority,
+ ];
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * implementedFinders
+ *
+ * Provides an alias->methodname map of which finders a behavior implements. Example:
+ *
+ * ```
+ * [
+ * 'this' => 'findThis',
+ * 'alias' => 'findMethodName'
+ * ]
+ * ```
+ *
+ * With the above example, a call to `$table->find('this')` will call `$behavior->findThis()`
+ * and a call to `$table->find('alias')` will call `$behavior->findMethodName()`
+ *
+ * It is recommended, though not required, to define implementedFinders in the config property
+ * of child classes such that it is not necessary to use reflections to derive the available
+ * method list. See core behaviors for examples
+ *
+ * @return array
+ * @throws \ReflectionException
+ */
+ public function implementedFinders(): array
+ {
+ $methods = $this->getConfig('implementedFinders');
+ if (isset($methods)) {
+ return $methods;
+ }
+
+ return $this->_reflectionCache()['finders'];
+ }
+
+ /**
+ * implementedMethods
+ *
+ * Provides an alias->methodname map of which methods a behavior implements. Example:
+ *
+ * ```
+ * [
+ * 'method' => 'method',
+ * 'aliasedMethod' => 'somethingElse'
+ * ]
+ * ```
+ *
+ * With the above example, a call to `$table->method()` will call `$behavior->method()`
+ * and a call to `$table->aliasedMethod()` will call `$behavior->somethingElse()`
+ *
+ * It is recommended, though not required, to define implementedFinders in the config property
+ * of child classes such that it is not necessary to use reflections to derive the available
+ * method list. See core behaviors for examples
+ *
+ * @return array
+ * @throws \ReflectionException
+ */
+ public function implementedMethods(): array
+ {
+ $methods = $this->getConfig('implementedMethods');
+ if (isset($methods)) {
+ return $methods;
+ }
+
+ return $this->_reflectionCache()['methods'];
+ }
+
+ /**
+ * Gets the methods implemented by this behavior
+ *
+ * Uses the implementedEvents() method to exclude callback methods.
+ * Methods starting with `_` will be ignored, as will methods
+ * declared on Cake\ORM\Behavior
+ *
+ * @return array
+ * @throws \ReflectionException
+ */
+ protected function _reflectionCache(): array
+ {
+ $class = static::class;
+ if (isset(self::$_reflectionCache[$class])) {
+ return self::$_reflectionCache[$class];
+ }
+
+ $events = $this->implementedEvents();
+ $eventMethods = [];
+ foreach ($events as $binding) {
+ if (is_array($binding) && isset($binding['callable'])) {
+ /** @var string $callable */
+ $callable = $binding['callable'];
+ $binding = $callable;
+ }
+ $eventMethods[$binding] = true;
+ }
+
+ $baseClass = self::class;
+ if (isset(self::$_reflectionCache[$baseClass])) {
+ $baseMethods = self::$_reflectionCache[$baseClass];
+ } else {
+ $baseMethods = get_class_methods($baseClass);
+ self::$_reflectionCache[$baseClass] = $baseMethods;
+ }
+
+ $return = [
+ 'finders' => [],
+ 'methods' => [],
+ ];
+
+ $reflection = new ReflectionClass($class);
+
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ $methodName = $method->getName();
+ if (
+ in_array($methodName, $baseMethods, true) ||
+ isset($eventMethods[$methodName])
+ ) {
+ continue;
+ }
+
+ if (substr($methodName, 0, 4) === 'find') {
+ $return['finders'][lcfirst(substr($methodName, 4))] = $methodName;
+ } else {
+ $return['methods'][$methodName] = $methodName;
+ }
+ }
+
+ return self::$_reflectionCache[$class] = $return;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/CounterCacheBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/CounterCacheBehavior.php
new file mode 100644
index 000000000..68746fb06
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/CounterCacheBehavior.php
@@ -0,0 +1,312 @@
+ [
+ * 'post_count'
+ * ]
+ * ]
+ * ```
+ *
+ * Counter cache with scope
+ * ```
+ * [
+ * 'Users' => [
+ * 'posts_published' => [
+ * 'conditions' => [
+ * 'published' => true
+ * ]
+ * ]
+ * ]
+ * ]
+ * ```
+ *
+ * Counter cache using custom find
+ * ```
+ * [
+ * 'Users' => [
+ * 'posts_published' => [
+ * 'finder' => 'published' // Will be using findPublished()
+ * ]
+ * ]
+ * ]
+ * ```
+ *
+ * Counter cache using lambda function returning the count
+ * This is equivalent to example #2
+ *
+ * ```
+ * [
+ * 'Users' => [
+ * 'posts_published' => function (EventInterface $event, EntityInterface $entity, Table $table) {
+ * $query = $table->find('all')->where([
+ * 'published' => true,
+ * 'user_id' => $entity->get('user_id')
+ * ]);
+ * return $query->count();
+ * }
+ * ]
+ * ]
+ * ```
+ *
+ * When using a lambda function you can return `false` to disable updating the counter value
+ * for the current operation.
+ *
+ * Ignore updating the field if it is dirty
+ * ```
+ * [
+ * 'Users' => [
+ * 'posts_published' => [
+ * 'ignoreDirty' => true
+ * ]
+ * ]
+ * ]
+ * ```
+ *
+ * You can disable counter updates entirely by sending the `ignoreCounterCache` option
+ * to your save operation:
+ *
+ * ```
+ * $this->Articles->save($article, ['ignoreCounterCache' => true]);
+ * ```
+ */
+class CounterCacheBehavior extends Behavior
+{
+ /**
+ * Store the fields which should be ignored
+ *
+ * @var array
+ */
+ protected $_ignoreDirty = [];
+
+ /**
+ * beforeSave callback.
+ *
+ * Check if a field, which should be ignored, is dirty
+ *
+ * @param \Cake\Event\EventInterface $event The beforeSave event that was fired
+ * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
+ * @param \ArrayObject $options The options for the query
+ * @return void
+ */
+ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
+ return;
+ }
+
+ foreach ($this->_config as $assoc => $settings) {
+ $assoc = $this->_table->getAssociation($assoc);
+ foreach ($settings as $field => $config) {
+ if (is_int($field)) {
+ continue;
+ }
+
+ $registryAlias = $assoc->getTarget()->getRegistryAlias();
+ $entityAlias = $assoc->getProperty();
+
+ if (
+ !is_callable($config) &&
+ isset($config['ignoreDirty']) &&
+ $config['ignoreDirty'] === true &&
+ $entity->$entityAlias->isDirty($field)
+ ) {
+ $this->_ignoreDirty[$registryAlias][$field] = true;
+ }
+ }
+ }
+ }
+
+ /**
+ * afterSave callback.
+ *
+ * Makes sure to update counter cache when a new record is created or updated.
+ *
+ * @param \Cake\Event\EventInterface $event The afterSave event that was fired.
+ * @param \Cake\Datasource\EntityInterface $entity The entity that was saved.
+ * @param \ArrayObject $options The options for the query
+ * @return void
+ */
+ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void
+ {
+ if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
+ return;
+ }
+
+ $this->_processAssociations($event, $entity);
+ $this->_ignoreDirty = [];
+ }
+
+ /**
+ * afterDelete callback.
+ *
+ * Makes sure to update counter cache when a record is deleted.
+ *
+ * @param \Cake\Event\EventInterface $event The afterDelete event that was fired.
+ * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted.
+ * @param \ArrayObject $options The options for the query
+ * @return void
+ */
+ public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
+ return;
+ }
+
+ $this->_processAssociations($event, $entity);
+ }
+
+ /**
+ * Iterate all associations and update counter caches.
+ *
+ * @param \Cake\Event\EventInterface $event Event instance.
+ * @param \Cake\Datasource\EntityInterface $entity Entity.
+ * @return void
+ */
+ protected function _processAssociations(EventInterface $event, EntityInterface $entity): void
+ {
+ foreach ($this->_config as $assoc => $settings) {
+ $assoc = $this->_table->getAssociation($assoc);
+ $this->_processAssociation($event, $entity, $assoc, $settings);
+ }
+ }
+
+ /**
+ * Updates counter cache for a single association
+ *
+ * @param \Cake\Event\EventInterface $event Event instance.
+ * @param \Cake\Datasource\EntityInterface $entity Entity
+ * @param \Cake\ORM\Association $assoc The association object
+ * @param array $settings The settings for for counter cache for this association
+ * @return void
+ * @throws \RuntimeException If invalid callable is passed.
+ */
+ protected function _processAssociation(
+ EventInterface $event,
+ EntityInterface $entity,
+ Association $assoc,
+ array $settings
+ ): void {
+ $foreignKeys = (array)$assoc->getForeignKey();
+ $countConditions = $entity->extract($foreignKeys);
+
+ foreach ($countConditions as $field => $value) {
+ if ($value === null) {
+ $countConditions[$field . ' IS'] = $value;
+ unset($countConditions[$field]);
+ }
+ }
+
+ $primaryKeys = (array)$assoc->getBindingKey();
+ $updateConditions = array_combine($primaryKeys, $countConditions);
+
+ $countOriginalConditions = $entity->extractOriginalChanged($foreignKeys);
+ if ($countOriginalConditions !== []) {
+ $updateOriginalConditions = array_combine($primaryKeys, $countOriginalConditions);
+ }
+
+ foreach ($settings as $field => $config) {
+ if (is_int($field)) {
+ $field = $config;
+ $config = [];
+ }
+
+ if (
+ isset($this->_ignoreDirty[$assoc->getTarget()->getRegistryAlias()][$field]) &&
+ $this->_ignoreDirty[$assoc->getTarget()->getRegistryAlias()][$field] === true
+ ) {
+ continue;
+ }
+
+ if ($this->_shouldUpdateCount($updateConditions)) {
+ if ($config instanceof Closure) {
+ $count = $config($event, $entity, $this->_table, false);
+ } else {
+ $count = $this->_getCount($config, $countConditions);
+ }
+ if ($count !== false) {
+ $assoc->getTarget()->updateAll([$field => $count], $updateConditions);
+ }
+ }
+
+ if (isset($updateOriginalConditions) && $this->_shouldUpdateCount($updateOriginalConditions)) {
+ if ($config instanceof Closure) {
+ $count = $config($event, $entity, $this->_table, true);
+ } else {
+ $count = $this->_getCount($config, $countOriginalConditions);
+ }
+ if ($count !== false) {
+ $assoc->getTarget()->updateAll([$field => $count], $updateOriginalConditions);
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the count should be updated given a set of conditions.
+ *
+ * @param array $conditions Conditions to update count.
+ * @return bool True if the count update should happen, false otherwise.
+ */
+ protected function _shouldUpdateCount(array $conditions)
+ {
+ return !empty(array_filter($conditions, function ($value) {
+ return $value !== null;
+ }));
+ }
+
+ /**
+ * Fetches and returns the count for a single field in an association
+ *
+ * @param array $config The counter cache configuration for a single field
+ * @param array $conditions Additional conditions given to the query
+ * @return int The number of relations matching the given config and conditions
+ */
+ protected function _getCount(array $config, array $conditions): int
+ {
+ $finder = 'all';
+ if (!empty($config['finder'])) {
+ $finder = $config['finder'];
+ unset($config['finder']);
+ }
+
+ if (!isset($config['conditions'])) {
+ $config['conditions'] = [];
+ }
+ $config['conditions'] = array_merge($conditions, $config['conditions']);
+ $query = $this->_table->find($finder, $config);
+
+ return $query->count();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/TimestampBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TimestampBehavior.php
new file mode 100644
index 000000000..d2b6f7f82
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TimestampBehavior.php
@@ -0,0 +1,230 @@
+ [],
+ 'implementedMethods' => [
+ 'timestamp' => 'timestamp',
+ 'touch' => 'touch',
+ ],
+ 'events' => [
+ 'Model.beforeSave' => [
+ 'created' => 'new',
+ 'modified' => 'always',
+ ],
+ ],
+ 'refreshTimestamp' => true,
+ ];
+
+ /**
+ * Current timestamp
+ *
+ * @var \Cake\I18n\Time|null
+ */
+ protected $_ts;
+
+ /**
+ * Initialize hook
+ *
+ * If events are specified - do *not* merge them with existing events,
+ * overwrite the events to listen on
+ *
+ * @param array $config The config for this behavior.
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ if (isset($config['events'])) {
+ $this->setConfig('events', $config['events'], false);
+ }
+ }
+
+ /**
+ * There is only one event handler, it can be configured to be called for any event
+ *
+ * @param \Cake\Event\EventInterface $event Event instance.
+ * @param \Cake\Datasource\EntityInterface $entity Entity instance.
+ * @throws \UnexpectedValueException if a field's when value is misdefined
+ * @return true Returns true irrespective of the behavior logic, the save will not be prevented.
+ * @throws \UnexpectedValueException When the value for an event is not 'always', 'new' or 'existing'
+ */
+ public function handleEvent(EventInterface $event, EntityInterface $entity): bool
+ {
+ $eventName = $event->getName();
+ $events = $this->_config['events'];
+
+ $new = $entity->isNew() !== false;
+ $refresh = $this->_config['refreshTimestamp'];
+
+ foreach ($events[$eventName] as $field => $when) {
+ if (!in_array($when, ['always', 'new', 'existing'], true)) {
+ throw new UnexpectedValueException(sprintf(
+ 'When should be one of "always", "new" or "existing". The passed value "%s" is invalid',
+ $when
+ ));
+ }
+ if (
+ $when === 'always' ||
+ (
+ $when === 'new' &&
+ $new
+ ) ||
+ (
+ $when === 'existing' &&
+ !$new
+ )
+ ) {
+ $this->_updateField($entity, $field, $refresh);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * implementedEvents
+ *
+ * The implemented events of this behavior depend on configuration
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ return array_fill_keys(array_keys($this->_config['events']), 'handleEvent');
+ }
+
+ /**
+ * Get or set the timestamp to be used
+ *
+ * Set the timestamp to the given DateTime object, or if not passed a new DateTime object
+ * If an explicit date time is passed, the config option `refreshTimestamp` is
+ * automatically set to false.
+ *
+ * @param \DateTimeInterface|null $ts Timestamp
+ * @param bool $refreshTimestamp If true timestamp is refreshed.
+ * @return \Cake\I18n\Time
+ */
+ public function timestamp(?DateTimeInterface $ts = null, bool $refreshTimestamp = false): DateTimeInterface
+ {
+ if ($ts) {
+ if ($this->_config['refreshTimestamp']) {
+ $this->_config['refreshTimestamp'] = false;
+ }
+ $this->_ts = new Time($ts);
+ } elseif ($this->_ts === null || $refreshTimestamp) {
+ $this->_ts = new Time();
+ }
+
+ return $this->_ts;
+ }
+
+ /**
+ * Touch an entity
+ *
+ * Bumps timestamp fields for an entity. For any fields configured to be updated
+ * "always" or "existing", update the timestamp value. This method will overwrite
+ * any pre-existing value.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity Entity instance.
+ * @param string $eventName Event name.
+ * @return bool true if a field is updated, false if no action performed
+ */
+ public function touch(EntityInterface $entity, string $eventName = 'Model.beforeSave'): bool
+ {
+ $events = $this->_config['events'];
+ if (empty($events[$eventName])) {
+ return false;
+ }
+
+ $return = false;
+ $refresh = $this->_config['refreshTimestamp'];
+
+ foreach ($events[$eventName] as $field => $when) {
+ if (in_array($when, ['always', 'existing'], true)) {
+ $return = true;
+ $entity->setDirty($field, false);
+ $this->_updateField($entity, $field, $refresh);
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Update a field, if it hasn't been updated already
+ *
+ * @param \Cake\Datasource\EntityInterface $entity Entity instance.
+ * @param string $field Field name
+ * @param bool $refreshTimestamp Whether to refresh timestamp.
+ * @return void
+ */
+ protected function _updateField(EntityInterface $entity, string $field, bool $refreshTimestamp): void
+ {
+ if ($entity->isDirty($field)) {
+ return;
+ }
+
+ $ts = $this->timestamp(null, $refreshTimestamp);
+
+ $columnType = $this->table()->getSchema()->getColumnType($field);
+ if (!$columnType) {
+ return;
+ }
+
+ /** @var \Cake\Database\Type\DateTimeType $type */
+ $type = TypeFactory::build($columnType);
+
+ if (!$type instanceof DateTimeType) {
+ throw new RuntimeException('TimestampBehavior only supports columns of type DateTimeType.');
+ }
+
+ $class = $type->getDateTimeClassName();
+
+ $entity->set($field, new $class($ts));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/EavStrategy.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/EavStrategy.php
new file mode 100644
index 000000000..2a9f0e46e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/EavStrategy.php
@@ -0,0 +1,515 @@
+ [],
+ 'translationTable' => 'I18n',
+ 'defaultLocale' => null,
+ 'referenceName' => null,
+ 'allowEmptyTranslations' => true,
+ 'onlyTranslated' => false,
+ 'strategy' => 'subquery',
+ 'tableLocator' => null,
+ 'validator' => false,
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\ORM\Table $table The table this strategy is attached to.
+ * @param array $config The config for this strategy.
+ */
+ public function __construct(Table $table, array $config = [])
+ {
+ if (isset($config['tableLocator'])) {
+ $this->_tableLocator = $config['tableLocator'];
+ }
+
+ $this->setConfig($config);
+ $this->table = $table;
+ $this->translationTable = $this->getTableLocator()->get(
+ $this->_config['translationTable'],
+ ['allowFallbackClass' => true]
+ );
+
+ $this->setupAssociations();
+ }
+
+ /**
+ * Creates the associations between the bound table and every field passed to
+ * this method.
+ *
+ * Additionally it creates a `i18n` HasMany association that will be
+ * used for fetching all translations for each record in the bound table.
+ *
+ * @return void
+ */
+ protected function setupAssociations()
+ {
+ $fields = $this->_config['fields'];
+ $table = $this->_config['translationTable'];
+ $model = $this->_config['referenceName'];
+ $strategy = $this->_config['strategy'];
+ $filter = $this->_config['onlyTranslated'];
+
+ $targetAlias = $this->translationTable->getAlias();
+ $alias = $this->table->getAlias();
+ $tableLocator = $this->getTableLocator();
+
+ foreach ($fields as $field) {
+ $name = $alias . '_' . $field . '_translation';
+
+ if (!$tableLocator->exists($name)) {
+ $fieldTable = $tableLocator->get($name, [
+ 'className' => $table,
+ 'alias' => $name,
+ 'table' => $this->translationTable->getTable(),
+ 'allowFallbackClass' => true,
+ ]);
+ } else {
+ $fieldTable = $tableLocator->get($name);
+ }
+
+ $conditions = [
+ $name . '.model' => $model,
+ $name . '.field' => $field,
+ ];
+ if (!$this->_config['allowEmptyTranslations']) {
+ $conditions[$name . '.content !='] = '';
+ }
+
+ $this->table->hasOne($name, [
+ 'targetTable' => $fieldTable,
+ 'foreignKey' => 'foreign_key',
+ 'joinType' => $filter ? Query::JOIN_TYPE_INNER : Query::JOIN_TYPE_LEFT,
+ 'conditions' => $conditions,
+ 'propertyName' => $field . '_translation',
+ ]);
+ }
+
+ $conditions = ["$targetAlias.model" => $model];
+ if (!$this->_config['allowEmptyTranslations']) {
+ $conditions["$targetAlias.content !="] = '';
+ }
+
+ $this->table->hasMany($targetAlias, [
+ 'className' => $table,
+ 'foreignKey' => 'foreign_key',
+ 'strategy' => $strategy,
+ 'conditions' => $conditions,
+ 'propertyName' => '_i18n',
+ 'dependent' => true,
+ ]);
+ }
+
+ /**
+ * Callback method that listens to the `beforeFind` event in the bound
+ * table. It modifies the passed query by eager loading the translated fields
+ * and adding a formatter to copy the values into the main table records.
+ *
+ * @param \Cake\Event\EventInterface $event The beforeFind event that was fired.
+ * @param \Cake\ORM\Query $query Query
+ * @param \ArrayObject $options The options for the query
+ * @return void
+ */
+ public function beforeFind(EventInterface $event, Query $query, ArrayObject $options)
+ {
+ $locale = Hash::get($options, 'locale', $this->getLocale());
+
+ if ($locale === $this->getConfig('defaultLocale')) {
+ return;
+ }
+
+ $conditions = function ($field, $locale, $query, $select) {
+ return function ($q) use ($field, $locale, $query, $select) {
+ $q->where([$q->getRepository()->aliasField('locale') => $locale]);
+
+ if (
+ $query->isAutoFieldsEnabled() ||
+ in_array($field, $select, true) ||
+ in_array($this->table->aliasField($field), $select, true)
+ ) {
+ $q->select(['id', 'content']);
+ }
+
+ return $q;
+ };
+ };
+
+ $contain = [];
+ $fields = $this->_config['fields'];
+ $alias = $this->table->getAlias();
+ $select = $query->clause('select');
+
+ $changeFilter = isset($options['filterByCurrentLocale']) &&
+ $options['filterByCurrentLocale'] !== $this->_config['onlyTranslated'];
+
+ foreach ($fields as $field) {
+ $name = $alias . '_' . $field . '_translation';
+
+ $contain[$name]['queryBuilder'] = $conditions(
+ $field,
+ $locale,
+ $query,
+ $select
+ );
+
+ if ($changeFilter) {
+ $filter = $options['filterByCurrentLocale']
+ ? Query::JOIN_TYPE_INNER
+ : Query::JOIN_TYPE_LEFT;
+ $contain[$name]['joinType'] = $filter;
+ }
+ }
+
+ $query->contain($contain);
+ $query->formatResults(function ($results) use ($locale) {
+ return $this->rowMapper($results, $locale);
+ }, $query::PREPEND);
+ }
+
+ /**
+ * Modifies the entity before it is saved so that translated fields are persisted
+ * in the database too.
+ *
+ * @param \Cake\Event\EventInterface $event The beforeSave event that was fired
+ * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
+ * @param \ArrayObject $options the options passed to the save method
+ * @return void
+ */
+ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ $locale = $entity->get('_locale') ?: $this->getLocale();
+ $newOptions = [$this->translationTable->getAlias() => ['validate' => false]];
+ $options['associated'] = $newOptions + $options['associated'];
+
+ // Check early if empty translations are present in the entity.
+ // If this is the case, unset them to prevent persistence.
+ // This only applies if $this->_config['allowEmptyTranslations'] is false
+ if ($this->_config['allowEmptyTranslations'] === false) {
+ $this->unsetEmptyFields($entity);
+ }
+
+ $this->bundleTranslatedFields($entity);
+ $bundled = $entity->get('_i18n') ?: [];
+ $noBundled = count($bundled) === 0;
+
+ // No additional translation records need to be saved,
+ // as the entity is in the default locale.
+ if ($noBundled && $locale === $this->getConfig('defaultLocale')) {
+ return;
+ }
+
+ $values = $entity->extract($this->_config['fields'], true);
+ $fields = array_keys($values);
+ $noFields = empty($fields);
+
+ // If there are no fields and no bundled translations, or both fields
+ // in the default locale and bundled translations we can
+ // skip the remaining logic as its not necessary.
+ if ($noFields && $noBundled || ($fields && $bundled)) {
+ return;
+ }
+
+ $primaryKey = (array)$this->table->getPrimaryKey();
+ $key = $entity->get(current($primaryKey));
+
+ // When we have no key and bundled translations, we
+ // need to mark the entity dirty so the root
+ // entity persists.
+ if ($noFields && $bundled && !$key) {
+ foreach ($this->_config['fields'] as $field) {
+ $entity->setDirty($field, true);
+ }
+
+ return;
+ }
+
+ if ($noFields) {
+ return;
+ }
+
+ $model = $this->_config['referenceName'];
+
+ $preexistent = [];
+ if ($key) {
+ $preexistent = $this->translationTable->find()
+ ->select(['id', 'field'])
+ ->where([
+ 'field IN' => $fields,
+ 'locale' => $locale,
+ 'foreign_key' => $key,
+ 'model' => $model,
+ ])
+ ->disableBufferedResults()
+ ->all()
+ ->indexBy('field');
+ }
+
+ $modified = [];
+ foreach ($preexistent as $field => $translation) {
+ $translation->set('content', $values[$field]);
+ $modified[$field] = $translation;
+ }
+
+ $new = array_diff_key($values, $modified);
+ foreach ($new as $field => $content) {
+ $new[$field] = new Entity(compact('locale', 'field', 'content', 'model'), [
+ 'useSetters' => false,
+ 'markNew' => true,
+ ]);
+ }
+
+ $entity->set('_i18n', array_merge($bundled, array_values($modified + $new)));
+ $entity->set('_locale', $locale, ['setter' => false]);
+ $entity->setDirty('_locale', false);
+
+ foreach ($fields as $field) {
+ $entity->setDirty($field, false);
+ }
+ }
+
+ /**
+ * Returns a fully aliased field name for translated fields.
+ *
+ * If the requested field is configured as a translation field, the `content`
+ * field with an alias of a corresponding association is returned. Table-aliased
+ * field name is returned for all other fields.
+ *
+ * @param string $field Field name to be aliased.
+ * @return string
+ */
+ public function translationField(string $field): string
+ {
+ $table = $this->table;
+ if ($this->getLocale() === $this->getConfig('defaultLocale')) {
+ return $table->aliasField($field);
+ }
+ $associationName = $table->getAlias() . '_' . $field . '_translation';
+
+ if ($table->associations()->has($associationName)) {
+ return $associationName . '.content';
+ }
+
+ return $table->aliasField($field);
+ }
+
+ /**
+ * Modifies the results from a table find in order to merge the translated fields
+ * into each entity for a given locale.
+ *
+ * @param \Cake\Datasource\ResultSetInterface $results Results to map.
+ * @param string $locale Locale string
+ * @return \Cake\Collection\CollectionInterface
+ */
+ protected function rowMapper($results, $locale)
+ {
+ return $results->map(function ($row) use ($locale) {
+ /** @var \Cake\Datasource\EntityInterface|array|null $row */
+ if ($row === null) {
+ return $row;
+ }
+ $hydrated = !is_array($row);
+
+ foreach ($this->_config['fields'] as $field) {
+ $name = $field . '_translation';
+ $translation = $row[$name] ?? null;
+
+ if ($translation === null || $translation === false) {
+ unset($row[$name]);
+ continue;
+ }
+
+ $content = $translation['content'] ?? null;
+ if ($content !== null) {
+ $row[$field] = $content;
+ }
+
+ unset($row[$name]);
+ }
+
+ $row['_locale'] = $locale;
+ if ($hydrated) {
+ /** @psalm-suppress PossiblyInvalidMethodCall */
+ $row->clean();
+ }
+
+ return $row;
+ });
+ }
+
+ /**
+ * Modifies the results from a table find in order to merge full translation
+ * records into each entity under the `_translations` key.
+ *
+ * @param \Cake\Datasource\ResultSetInterface $results Results to modify.
+ * @return \Cake\Collection\CollectionInterface
+ */
+ public function groupTranslations($results): CollectionInterface
+ {
+ return $results->map(function ($row) {
+ if (!$row instanceof EntityInterface) {
+ return $row;
+ }
+ $translations = (array)$row->get('_i18n');
+ if (empty($translations) && $row->get('_translations')) {
+ return $row;
+ }
+ $grouped = new Collection($translations);
+
+ $result = [];
+ foreach ($grouped->combine('field', 'content', 'locale') as $locale => $keys) {
+ $entityClass = $this->table->getEntityClass();
+ $translation = new $entityClass($keys + ['locale' => $locale], [
+ 'markNew' => false,
+ 'useSetters' => false,
+ 'markClean' => true,
+ ]);
+ $result[$locale] = $translation;
+ }
+
+ $options = ['setter' => false, 'guard' => false];
+ $row->set('_translations', $result, $options);
+ unset($row['_i18n']);
+ $row->clean();
+
+ return $row;
+ });
+ }
+
+ /**
+ * Helper method used to generated multiple translated field entities
+ * out of the data found in the `_translations` property in the passed
+ * entity. The result will be put into its `_i18n` property.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity Entity
+ * @return void
+ */
+ protected function bundleTranslatedFields($entity)
+ {
+ $translations = (array)$entity->get('_translations');
+
+ if (empty($translations) && !$entity->isDirty('_translations')) {
+ return;
+ }
+
+ $fields = $this->_config['fields'];
+ $primaryKey = (array)$this->table->getPrimaryKey();
+ $key = $entity->get(current($primaryKey));
+ $find = [];
+ $contents = [];
+
+ foreach ($translations as $lang => $translation) {
+ foreach ($fields as $field) {
+ if (!$translation->isDirty($field)) {
+ continue;
+ }
+ $find[] = ['locale' => $lang, 'field' => $field, 'foreign_key IS' => $key];
+ $contents[] = new Entity(['content' => $translation->get($field)], [
+ 'useSetters' => false,
+ ]);
+ }
+ }
+
+ if (empty($find)) {
+ return;
+ }
+
+ $results = $this->findExistingTranslations($find);
+
+ foreach ($find as $i => $translation) {
+ if (!empty($results[$i])) {
+ $contents[$i]->set('id', $results[$i], ['setter' => false]);
+ $contents[$i]->setNew(false);
+ } else {
+ $translation['model'] = $this->_config['referenceName'];
+ $contents[$i]->set($translation, ['setter' => false, 'guard' => false]);
+ $contents[$i]->setNew(true);
+ }
+ }
+
+ $entity->set('_i18n', $contents);
+ }
+
+ /**
+ * Returns the ids found for each of the condition arrays passed for the
+ * translations table. Each records is indexed by the corresponding position
+ * to the conditions array.
+ *
+ * @param array $ruleSet An array of array of conditions to be used for finding each
+ * @return array
+ */
+ protected function findExistingTranslations($ruleSet)
+ {
+ $association = $this->table->getAssociation($this->translationTable->getAlias());
+
+ $query = $association->find()
+ ->select(['id', 'num' => 0])
+ ->where(current($ruleSet))
+ ->disableHydration()
+ ->disableBufferedResults();
+
+ unset($ruleSet[0]);
+ foreach ($ruleSet as $i => $conditions) {
+ $q = $association->find()
+ ->select(['id', 'num' => $i])
+ ->where($conditions);
+ $query->unionAll($q);
+ }
+
+ return $query->all()->combine('num', 'id')->toArray();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/ShadowTableStrategy.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/ShadowTableStrategy.php
new file mode 100644
index 000000000..efcfecee4
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/ShadowTableStrategy.php
@@ -0,0 +1,629 @@
+ [],
+ 'defaultLocale' => null,
+ 'referenceName' => null,
+ 'allowEmptyTranslations' => true,
+ 'onlyTranslated' => false,
+ 'strategy' => 'subquery',
+ 'tableLocator' => null,
+ 'validator' => false,
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\ORM\Table $table Table instance.
+ * @param array $config Configuration.
+ */
+ public function __construct(Table $table, array $config = [])
+ {
+ $tableAlias = $table->getAlias();
+ [$plugin] = pluginSplit($table->getRegistryAlias(), true);
+ $tableReferenceName = $config['referenceName'];
+
+ $config += [
+ 'mainTableAlias' => $tableAlias,
+ 'translationTable' => $plugin . $tableReferenceName . 'Translations',
+ 'hasOneAlias' => $tableAlias . 'Translation',
+ ];
+
+ if (isset($config['tableLocator'])) {
+ $this->_tableLocator = $config['tableLocator'];
+ }
+
+ $this->setConfig($config);
+ $this->table = $table;
+ $this->translationTable = $this->getTableLocator()->get(
+ $this->_config['translationTable'],
+ ['allowFallbackClass' => true]
+ );
+
+ $this->setupAssociations();
+ }
+
+ /**
+ * Create a hasMany association for all records.
+ *
+ * Don't create a hasOne association here as the join conditions are modified
+ * in before find - so create/modify it there.
+ *
+ * @return void
+ */
+ protected function setupAssociations()
+ {
+ $config = $this->getConfig();
+
+ $targetAlias = $this->translationTable->getAlias();
+ $this->table->hasMany($targetAlias, [
+ 'className' => $config['translationTable'],
+ 'foreignKey' => 'id',
+ 'strategy' => $config['strategy'],
+ 'propertyName' => '_i18n',
+ 'dependent' => true,
+ ]);
+ }
+
+ /**
+ * Callback method that listens to the `beforeFind` event in the bound
+ * table. It modifies the passed query by eager loading the translated fields
+ * and adding a formatter to copy the values into the main table records.
+ *
+ * @param \Cake\Event\EventInterface $event The beforeFind event that was fired.
+ * @param \Cake\ORM\Query $query Query.
+ * @param \ArrayObject $options The options for the query.
+ * @return void
+ */
+ public function beforeFind(EventInterface $event, Query $query, ArrayObject $options)
+ {
+ $locale = Hash::get($options, 'locale', $this->getLocale());
+ $config = $this->getConfig();
+
+ if ($locale === $config['defaultLocale']) {
+ return;
+ }
+
+ $this->setupHasOneAssociation($locale, $options);
+
+ $fieldsAdded = $this->addFieldsToQuery($query, $config);
+ $orderByTranslatedField = $this->iterateClause($query, 'order', $config);
+ $filteredByTranslatedField = $this->traverseClause($query, 'where', $config);
+
+ if (!$fieldsAdded && !$orderByTranslatedField && !$filteredByTranslatedField) {
+ return;
+ }
+
+ $query->contain([$config['hasOneAlias']]);
+
+ $query->formatResults(function ($results) use ($locale) {
+ return $this->rowMapper($results, $locale);
+ }, $query::PREPEND);
+ }
+
+ /**
+ * Create a hasOne association for record with required locale.
+ *
+ * @param string $locale Locale
+ * @param \ArrayObject $options Find options
+ * @return void
+ */
+ protected function setupHasOneAssociation(string $locale, ArrayObject $options): void
+ {
+ $config = $this->getConfig();
+
+ [$plugin] = pluginSplit($config['translationTable']);
+ $hasOneTargetAlias = $plugin ? ($plugin . '.' . $config['hasOneAlias']) : $config['hasOneAlias'];
+ if (!$this->getTableLocator()->exists($hasOneTargetAlias)) {
+ // Load table before hand with fallback class usage enabled
+ $this->getTableLocator()->get(
+ $hasOneTargetAlias,
+ [
+ 'className' => $config['translationTable'],
+ 'allowFallbackClass' => true,
+ ]
+ );
+ }
+
+ if (isset($options['filterByCurrentLocale'])) {
+ $joinType = $options['filterByCurrentLocale'] ? 'INNER' : 'LEFT';
+ } else {
+ $joinType = $config['onlyTranslated'] ? 'INNER' : 'LEFT';
+ }
+
+ $this->table->hasOne($config['hasOneAlias'], [
+ 'foreignKey' => ['id'],
+ 'joinType' => $joinType,
+ 'propertyName' => 'translation',
+ 'className' => $config['translationTable'],
+ 'conditions' => [
+ $config['hasOneAlias'] . '.locale' => $locale,
+ ],
+ ]);
+ }
+
+ /**
+ * Add translation fields to query.
+ *
+ * If the query is using autofields (directly or implicitly) add the
+ * main table's fields to the query first.
+ *
+ * Only add translations for fields that are in the main table, always
+ * add the locale field though.
+ *
+ * @param \Cake\ORM\Query $query The query to check.
+ * @param array $config The config to use for adding fields.
+ * @return bool Whether a join to the translation table is required.
+ */
+ protected function addFieldsToQuery($query, array $config)
+ {
+ if ($query->isAutoFieldsEnabled()) {
+ return true;
+ }
+
+ $select = array_filter($query->clause('select'), function ($field) {
+ return is_string($field);
+ });
+
+ if (!$select) {
+ return true;
+ }
+
+ $alias = $config['mainTableAlias'];
+ $joinRequired = false;
+ foreach ($this->translatedFields() as $field) {
+ if (array_intersect($select, [$field, "$alias.$field"])) {
+ $joinRequired = true;
+ $query->select($query->aliasField($field, $config['hasOneAlias']));
+ }
+ }
+
+ if ($joinRequired) {
+ $query->select($query->aliasField('locale', $config['hasOneAlias']));
+ }
+
+ return $joinRequired;
+ }
+
+ /**
+ * Iterate over a clause to alias fields.
+ *
+ * The objective here is to transparently prevent ambiguous field errors by
+ * prefixing fields with the appropriate table alias. This method currently
+ * expects to receive an order clause only.
+ *
+ * @param \Cake\ORM\Query $query the query to check.
+ * @param string $name The clause name.
+ * @param array $config The config to use for adding fields.
+ * @return bool Whether a join to the translation table is required.
+ */
+ protected function iterateClause($query, $name = '', $config = []): bool
+ {
+ $clause = $query->clause($name);
+ if (!$clause || !$clause->count()) {
+ return false;
+ }
+
+ $alias = $config['hasOneAlias'];
+ $fields = $this->translatedFields();
+ $mainTableAlias = $config['mainTableAlias'];
+ $mainTableFields = $this->mainFields();
+ $joinRequired = false;
+
+ $clause->iterateParts(
+ function ($c, &$field) use ($fields, $alias, $mainTableAlias, $mainTableFields, &$joinRequired) {
+ if (!is_string($field) || strpos($field, '.')) {
+ return $c;
+ }
+
+ if (in_array($field, $fields, true)) {
+ $joinRequired = true;
+ $field = "$alias.$field";
+ } elseif (in_array($field, $mainTableFields, true)) {
+ $field = "$mainTableAlias.$field";
+ }
+
+ return $c;
+ }
+ );
+
+ return $joinRequired;
+ }
+
+ /**
+ * Traverse over a clause to alias fields.
+ *
+ * The objective here is to transparently prevent ambiguous field errors by
+ * prefixing fields with the appropriate table alias. This method currently
+ * expects to receive a where clause only.
+ *
+ * @param \Cake\ORM\Query $query the query to check.
+ * @param string $name The clause name.
+ * @param array $config The config to use for adding fields.
+ * @return bool Whether a join to the translation table is required.
+ */
+ protected function traverseClause($query, $name = '', $config = []): bool
+ {
+ $clause = $query->clause($name);
+ if (!$clause || !$clause->count()) {
+ return false;
+ }
+
+ $alias = $config['hasOneAlias'];
+ $fields = $this->translatedFields();
+ $mainTableAlias = $config['mainTableAlias'];
+ $mainTableFields = $this->mainFields();
+ $joinRequired = false;
+
+ $clause->traverse(
+ function ($expression) use ($fields, $alias, $mainTableAlias, $mainTableFields, &$joinRequired) {
+ if (!($expression instanceof FieldInterface)) {
+ return;
+ }
+ $field = $expression->getField();
+ if (!is_string($field) || strpos($field, '.')) {
+ return;
+ }
+
+ if (in_array($field, $fields, true)) {
+ $joinRequired = true;
+ $expression->setField("$alias.$field");
+
+ return;
+ }
+
+ if (in_array($field, $mainTableFields, true)) {
+ $expression->setField("$mainTableAlias.$field");
+ }
+ }
+ );
+
+ return $joinRequired;
+ }
+
+ /**
+ * Modifies the entity before it is saved so that translated fields are persisted
+ * in the database too.
+ *
+ * @param \Cake\Event\EventInterface $event The beforeSave event that was fired.
+ * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved.
+ * @param \ArrayObject $options the options passed to the save method.
+ * @return void
+ */
+ public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
+ {
+ $locale = $entity->get('_locale') ?: $this->getLocale();
+ $newOptions = [$this->translationTable->getAlias() => ['validate' => false]];
+ $options['associated'] = $newOptions + $options['associated'];
+
+ // Check early if empty translations are present in the entity.
+ // If this is the case, unset them to prevent persistence.
+ // This only applies if $this->_config['allowEmptyTranslations'] is false
+ if ($this->_config['allowEmptyTranslations'] === false) {
+ $this->unsetEmptyFields($entity);
+ }
+
+ $this->bundleTranslatedFields($entity);
+ $bundled = $entity->get('_i18n') ?: [];
+ $noBundled = count($bundled) === 0;
+
+ // No additional translation records need to be saved,
+ // as the entity is in the default locale.
+ if ($noBundled && $locale === $this->getConfig('defaultLocale')) {
+ return;
+ }
+
+ $values = $entity->extract($this->translatedFields(), true);
+ $fields = array_keys($values);
+ $noFields = empty($fields);
+
+ // If there are no fields and no bundled translations, or both fields
+ // in the default locale and bundled translations we can
+ // skip the remaining logic as its not necessary.
+ if ($noFields && $noBundled || ($fields && $bundled)) {
+ return;
+ }
+
+ $primaryKey = (array)$this->table->getPrimaryKey();
+ $id = $entity->get(current($primaryKey));
+
+ // When we have no key and bundled translations, we
+ // need to mark the entity dirty so the root
+ // entity persists.
+ if ($noFields && $bundled && !$id) {
+ foreach ($this->translatedFields() as $field) {
+ $entity->setDirty($field, true);
+ }
+
+ return;
+ }
+
+ if ($noFields) {
+ return;
+ }
+
+ $where = ['locale' => $locale];
+ $translation = null;
+ if ($id) {
+ $where['id'] = $id;
+
+ /** @var \Cake\Datasource\EntityInterface|null $translation */
+ $translation = $this->translationTable->find()
+ ->select(array_merge(['id', 'locale'], $fields))
+ ->where($where)
+ ->disableBufferedResults()
+ ->first();
+ }
+
+ if ($translation) {
+ $translation->set($values);
+ } else {
+ $translation = $this->translationTable->newEntity(
+ $where + $values,
+ [
+ 'useSetters' => false,
+ 'markNew' => true,
+ ]
+ );
+ }
+
+ $entity->set('_i18n', array_merge($bundled, [$translation]));
+ $entity->set('_locale', $locale, ['setter' => false]);
+ $entity->setDirty('_locale', false);
+
+ foreach ($fields as $field) {
+ $entity->setDirty($field, false);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array
+ {
+ $this->translatedFields();
+
+ return $this->_buildMarshalMap($marshaller, $map, $options);
+ }
+
+ /**
+ * Returns a fully aliased field name for translated fields.
+ *
+ * If the requested field is configured as a translation field, field with
+ * an alias of a corresponding association is returned. Table-aliased
+ * field name is returned for all other fields.
+ *
+ * @param string $field Field name to be aliased.
+ * @return string
+ */
+ public function translationField(string $field): string
+ {
+ if ($this->getLocale() === $this->getConfig('defaultLocale')) {
+ return $this->table->aliasField($field);
+ }
+
+ $translatedFields = $this->translatedFields();
+ if (in_array($field, $translatedFields, true)) {
+ return $this->getConfig('hasOneAlias') . '.' . $field;
+ }
+
+ return $this->table->aliasField($field);
+ }
+
+ /**
+ * Modifies the results from a table find in order to merge the translated
+ * fields into each entity for a given locale.
+ *
+ * @param \Cake\Datasource\ResultSetInterface $results Results to map.
+ * @param string $locale Locale string
+ * @return \Cake\Collection\CollectionInterface
+ */
+ protected function rowMapper($results, $locale)
+ {
+ $allowEmpty = $this->_config['allowEmptyTranslations'];
+
+ return $results->map(function ($row) use ($allowEmpty, $locale) {
+ /** @var \Cake\Datasource\EntityInterface|array|null $row */
+ if ($row === null) {
+ return $row;
+ }
+
+ $hydrated = !is_array($row);
+
+ if (empty($row['translation'])) {
+ $row['_locale'] = $locale;
+ unset($row['translation']);
+
+ if ($hydrated) {
+ /** @psalm-suppress PossiblyInvalidMethodCall */
+ $row->clean();
+ }
+
+ return $row;
+ }
+
+ /** @var \Cake\ORM\Entity|array $translation */
+ $translation = $row['translation'];
+
+ /**
+ * @psalm-suppress PossiblyInvalidMethodCall
+ * @psalm-suppress PossiblyInvalidArgument
+ */
+ $keys = $hydrated ? $translation->getVisible() : array_keys($translation);
+
+ foreach ($keys as $field) {
+ if ($field === 'locale') {
+ $row['_locale'] = $translation[$field];
+ continue;
+ }
+
+ if ($translation[$field] !== null) {
+ if ($allowEmpty || $translation[$field] !== '') {
+ $row[$field] = $translation[$field];
+ }
+ }
+ }
+
+ unset($row['translation']);
+
+ if ($hydrated) {
+ /** @psalm-suppress PossiblyInvalidMethodCall */
+ $row->clean();
+ }
+
+ return $row;
+ });
+ }
+
+ /**
+ * Modifies the results from a table find in order to merge full translation
+ * records into each entity under the `_translations` key.
+ *
+ * @param \Cake\Datasource\ResultSetInterface $results Results to modify.
+ * @return \Cake\Collection\CollectionInterface
+ */
+ public function groupTranslations($results): CollectionInterface
+ {
+ return $results->map(function ($row) {
+ $translations = (array)$row['_i18n'];
+ if (empty($translations) && $row->get('_translations')) {
+ return $row;
+ }
+
+ $result = [];
+ foreach ($translations as $translation) {
+ unset($translation['id']);
+ $result[$translation['locale']] = $translation;
+ }
+
+ $row['_translations'] = $result;
+ unset($row['_i18n']);
+ if ($row instanceof EntityInterface) {
+ $row->clean();
+ }
+
+ return $row;
+ });
+ }
+
+ /**
+ * Helper method used to generated multiple translated field entities
+ * out of the data found in the `_translations` property in the passed
+ * entity. The result will be put into its `_i18n` property.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity Entity.
+ * @return void
+ */
+ protected function bundleTranslatedFields($entity)
+ {
+ $translations = (array)$entity->get('_translations');
+
+ if (empty($translations) && !$entity->isDirty('_translations')) {
+ return;
+ }
+
+ $primaryKey = (array)$this->table->getPrimaryKey();
+ $key = $entity->get(current($primaryKey));
+
+ foreach ($translations as $lang => $translation) {
+ if (!$translation->id) {
+ $update = [
+ 'id' => $key,
+ 'locale' => $lang,
+ ];
+ $translation->set($update, ['guard' => false]);
+ }
+ }
+
+ $entity->set('_i18n', $translations);
+ }
+
+ /**
+ * Lazy define and return the main table fields.
+ *
+ * @return array
+ */
+ protected function mainFields()
+ {
+ $fields = $this->getConfig('mainTableFields');
+
+ if ($fields) {
+ return $fields;
+ }
+
+ $fields = $this->table->getSchema()->columns();
+
+ $this->setConfig('mainTableFields', $fields);
+
+ return $fields;
+ }
+
+ /**
+ * Lazy define and return the translation table fields.
+ *
+ * @return array
+ */
+ protected function translatedFields()
+ {
+ $fields = $this->getConfig('fields');
+
+ if ($fields) {
+ return $fields;
+ }
+
+ $table = $this->translationTable;
+ $fields = $table->getSchema()->columns();
+ $fields = array_values(array_diff($fields, ['id', 'locale']));
+
+ $this->setConfig('fields', $fields);
+
+ return $fields;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateStrategyInterface.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateStrategyInterface.php
new file mode 100644
index 000000000..9f8d9a200
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateStrategyInterface.php
@@ -0,0 +1,118 @@
+translationTable;
+ }
+
+ /**
+ * Sets the locale to be used.
+ *
+ * When fetching records, the content for the locale set via this method,
+ * and likewise when saving data, it will save the data in that locale.
+ *
+ * Note that in case an entity has a `_locale` property set, that locale
+ * will win over the locale set via this method (and over the globally
+ * configured one for that matter)!
+ *
+ * @param string|null $locale The locale to use for fetching and saving
+ * records. Pass `null` in order to unset the current locale, and to make
+ * the behavior fall back to using the globally configured locale.
+ * @return $this
+ */
+ public function setLocale(?string $locale)
+ {
+ $this->locale = $locale;
+
+ return $this;
+ }
+
+ /**
+ * Returns the current locale.
+ *
+ * If no locale has been explicitly set via `setLocale()`, this method will return
+ * the currently configured global locale.
+ *
+ * @return string
+ * @see \Cake\I18n\I18n::getLocale()
+ * @see \Cake\ORM\Behavior\TranslateBehavior::setLocale()
+ */
+ public function getLocale(): string
+ {
+ return $this->locale ?: I18n::getLocale();
+ }
+
+ /**
+ * Unset empty translations to avoid persistence.
+ *
+ * Should only be called if $this->_config['allowEmptyTranslations'] is false.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check for empty translations fields inside.
+ * @return void
+ */
+ protected function unsetEmptyFields($entity)
+ {
+ /** @var \Cake\ORM\Entity[] $translations */
+ $translations = (array)$entity->get('_translations');
+ foreach ($translations as $locale => $translation) {
+ $fields = $translation->extract($this->_config['fields'], false);
+ foreach ($fields as $field => $value) {
+ if ($value === null || $value === '') {
+ $translation->unset($field);
+ }
+ }
+
+ $translation = $translation->extract($this->_config['fields']);
+
+ // If now, the current locale property is empty,
+ // unset it completely.
+ if (empty(array_filter($translation))) {
+ unset($entity->get('_translations')[$locale]);
+ }
+ }
+
+ // If now, the whole _translations property is empty,
+ // unset it completely and return
+ if (empty($entity->get('_translations'))) {
+ $entity->unset('_translations');
+ }
+ }
+
+ /**
+ * Build a set of properties that should be included in the marshalling process.
+
+ * Add in `_translations` marshalling handlers. You can disable marshalling
+ * of translations by setting `'translations' => false` in the options
+ * provided to `Table::newEntity()` or `Table::patchEntity()`.
+ *
+ * @param \Cake\ORM\Marshaller $marshaller The marhshaller of the table the behavior is attached to.
+ * @param array $map The property map being built.
+ * @param array $options The options array used in the marshalling call.
+ * @return array A map of `[property => callable]` of additional properties to marshal.
+ */
+ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array
+ {
+ if (isset($options['translations']) && !$options['translations']) {
+ return [];
+ }
+
+ return [
+ '_translations' => function ($value, $entity) use ($marshaller, $options) {
+ if (!is_array($value)) {
+ return null;
+ }
+
+ /** @var array|null $translations */
+ $translations = $entity->get('_translations');
+ if ($translations === null) {
+ $translations = [];
+ }
+
+ $options['validate'] = $this->_config['validator'];
+ $errors = [];
+ foreach ($value as $language => $fields) {
+ if (!isset($translations[$language])) {
+ $translations[$language] = $this->table->newEmptyEntity();
+ }
+ $marshaller->merge($translations[$language], $fields, $options);
+
+ $translationErrors = $translations[$language]->getErrors();
+ if ($translationErrors) {
+ $errors[$language] = $translationErrors;
+ }
+ }
+
+ // Set errors into the root entity, so validation errors match the original form data position.
+ if ($errors) {
+ $entity->setErrors($errors);
+ }
+
+ return $translations;
+ },
+ ];
+ }
+
+ /**
+ * Unsets the temporary `_i18n` property after the entity has been saved
+ *
+ * @param \Cake\Event\EventInterface $event The beforeSave event that was fired
+ * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
+ * @return void
+ */
+ public function afterSave(EventInterface $event, EntityInterface $entity)
+ {
+ $entity->unset('_i18n');
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateTrait.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateTrait.php
new file mode 100644
index 000000000..23670a313
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/Translate/TranslateTrait.php
@@ -0,0 +1,66 @@
+get('_locale')) {
+ return $this;
+ }
+
+ $i18n = $this->get('_translations');
+ $created = false;
+
+ if (empty($i18n)) {
+ $i18n = [];
+ $created = true;
+ }
+
+ if ($created || empty($i18n[$language]) || !($i18n[$language] instanceof EntityInterface)) {
+ $className = static::class;
+
+ $i18n[$language] = new $className();
+ $created = true;
+ }
+
+ if ($created) {
+ $this->set('_translations', $i18n);
+ }
+
+ // Assume the user will modify any of the internal translations, helps with saving
+ $this->setDirty('_translations', true);
+
+ return $i18n[$language];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php
new file mode 100644
index 000000000..5daecbb6b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TranslateBehavior.php
@@ -0,0 +1,363 @@
+ ['translations' => 'findTranslations'],
+ 'implementedMethods' => [
+ 'setLocale' => 'setLocale',
+ 'getLocale' => 'getLocale',
+ 'translationField' => 'translationField',
+ ],
+ 'fields' => [],
+ 'defaultLocale' => null,
+ 'referenceName' => '',
+ 'allowEmptyTranslations' => true,
+ 'onlyTranslated' => false,
+ 'strategy' => 'subquery',
+ 'tableLocator' => null,
+ 'validator' => false,
+ ];
+
+ /**
+ * Default strategy class name.
+ *
+ * @var string
+ * @psalm-var class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface>
+ */
+ protected static $defaultStrategyClass = EavStrategy::class;
+
+ /**
+ * Translation strategy instance.
+ *
+ * @var \Cake\ORM\Behavior\Translate\TranslateStrategyInterface|null
+ */
+ protected $strategy;
+
+ /**
+ * Constructor
+ *
+ * ### Options
+ *
+ * - `fields`: List of fields which need to be translated. Providing this fields
+ * list is mandatory when using `EavStrategy`. If the fields list is empty when
+ * using `ShadowTableStrategy` then the list will be auto generated based on
+ * shadow table schema.
+ * - `defaultLocale`: The locale which is treated as default by the behavior.
+ * Fields values for defaut locale will be stored in the primary table itself
+ * and the rest in translation table. If not explicitly set the value of
+ * `I18n::getDefaultLocale()` will be used to get default locale.
+ * If you do not want any default locale and want translated fields
+ * for all locales to be stored in translation table then set this config
+ * to empty string `''`.
+ * - `allowEmptyTranslations`: By default if a record has been translated and
+ * stored as an empty string the translate behavior will take and use this
+ * value to overwrite the original field value. If you don't want this behavior
+ * then set this option to `false`.
+ * - `validator`: The validator that should be used when translation records
+ * are created/modified. Default `null`.
+ *
+ * @param \Cake\ORM\Table $table The table this behavior is attached to.
+ * @param array $config The config for this behavior.
+ */
+ public function __construct(Table $table, array $config = [])
+ {
+ $config += [
+ 'defaultLocale' => I18n::getDefaultLocale(),
+ 'referenceName' => $this->referenceName($table),
+ 'tableLocator' => $table->associations()->getTableLocator(),
+ ];
+
+ parent::__construct($table, $config);
+ }
+
+ /**
+ * Initialize hook
+ *
+ * @param array $config The config for this behavior.
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ $this->getStrategy();
+ }
+
+ /**
+ * Set default strategy class name.
+ *
+ * @param string $class Class name.
+ * @return void
+ * @since 4.0.0
+ * @psalm-param class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface> $class
+ */
+ public static function setDefaultStrategyClass(string $class)
+ {
+ static::$defaultStrategyClass = $class;
+ }
+
+ /**
+ * Get default strategy class name.
+ *
+ * @return string
+ * @since 4.0.0
+ * @psalm-return class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface>
+ */
+ public static function getDefaultStrategyClass(): string
+ {
+ return static::$defaultStrategyClass;
+ }
+
+ /**
+ * Get strategy class instance.
+ *
+ * @return \Cake\ORM\Behavior\Translate\TranslateStrategyInterface
+ * @since 4.0.0
+ */
+ public function getStrategy(): TranslateStrategyInterface
+ {
+ if ($this->strategy !== null) {
+ return $this->strategy;
+ }
+
+ return $this->strategy = $this->createStrategy();
+ }
+
+ /**
+ * Create strategy instance.
+ *
+ * @return \Cake\ORM\Behavior\Translate\TranslateStrategyInterface
+ * @since 4.0.0
+ */
+ protected function createStrategy()
+ {
+ $config = array_diff_key(
+ $this->_config,
+ ['implementedFinders', 'implementedMethods', 'strategyClass']
+ );
+ /** @var class-string<\Cake\ORM\Behavior\Translate\TranslateStrategyInterface> $className */
+ $className = $this->getConfig('strategyClass', static::$defaultStrategyClass);
+
+ return new $className($this->_table, $config);
+ }
+
+ /**
+ * Set strategy class instance.
+ *
+ * @param \Cake\ORM\Behavior\Translate\TranslateStrategyInterface $strategy Strategy class instance.
+ * @return $this
+ * @since 4.0.0
+ */
+ public function setStrategy(TranslateStrategyInterface $strategy)
+ {
+ $this->strategy = $strategy;
+
+ return $this;
+ }
+
+ /**
+ * Gets the Model callbacks this behavior is interested in.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ return [
+ 'Model.beforeFind' => 'beforeFind',
+ 'Model.beforeSave' => 'beforeSave',
+ 'Model.afterSave' => 'afterSave',
+ ];
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Add in `_translations` marshalling handlers. You can disable marshalling
+ * of translations by setting `'translations' => false` in the options
+ * provided to `Table::newEntity()` or `Table::patchEntity()`.
+ *
+ * @param \Cake\ORM\Marshaller $marshaller The marhshaller of the table the behavior is attached to.
+ * @param array $map The property map being built.
+ * @param array $options The options array used in the marshalling call.
+ * @return array A map of `[property => callable]` of additional properties to marshal.
+ */
+ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array
+ {
+ return $this->getStrategy()->buildMarshalMap($marshaller, $map, $options);
+ }
+
+ /**
+ * Sets the locale that should be used for all future find and save operations on
+ * the table where this behavior is attached to.
+ *
+ * When fetching records, the behavior will include the content for the locale set
+ * via this method, and likewise when saving data, it will save the data in that
+ * locale.
+ *
+ * Note that in case an entity has a `_locale` property set, that locale will win
+ * over the locale set via this method (and over the globally configured one for
+ * that matter)!
+ *
+ * @param string|null $locale The locale to use for fetching and saving records. Pass `null`
+ * in order to unset the current locale, and to make the behavior fall back to using the
+ * globally configured locale.
+ * @return $this
+ * @see \Cake\ORM\Behavior\TranslateBehavior::getLocale()
+ * @link https://book.cakephp.org/4/en/orm/behaviors/translate.html#retrieving-one-language-without-using-i18n-locale
+ * @link https://book.cakephp.org/4/en/orm/behaviors/translate.html#saving-in-another-language
+ */
+ public function setLocale(?string $locale)
+ {
+ $this->getStrategy()->setLocale($locale);
+
+ return $this;
+ }
+
+ /**
+ * Returns the current locale.
+ *
+ * If no locale has been explicitly set via `setLocale()`, this method will return
+ * the currently configured global locale.
+ *
+ * @return string
+ * @see \Cake\I18n\I18n::getLocale()
+ * @see \Cake\ORM\Behavior\TranslateBehavior::setLocale()
+ */
+ public function getLocale(): string
+ {
+ return $this->getStrategy()->getLocale();
+ }
+
+ /**
+ * Returns a fully aliased field name for translated fields.
+ *
+ * If the requested field is configured as a translation field, the `content`
+ * field with an alias of a corresponding association is returned. Table-aliased
+ * field name is returned for all other fields.
+ *
+ * @param string $field Field name to be aliased.
+ * @return string
+ */
+ public function translationField(string $field): string
+ {
+ return $this->getStrategy()->translationField($field);
+ }
+
+ /**
+ * Custom finder method used to retrieve all translations for the found records.
+ * Fetched translations can be filtered by locale by passing the `locales` key
+ * in the options array.
+ *
+ * Translated values will be found for each entity under the property `_translations`,
+ * containing an array indexed by locale name.
+ *
+ * ### Example:
+ *
+ * ```
+ * $article = $articles->find('translations', ['locales' => ['eng', 'deu'])->first();
+ * $englishTranslatedFields = $article->get('_translations')['eng'];
+ * ```
+ *
+ * If the `locales` array is not passed, it will bring all translations found
+ * for each record.
+ *
+ * @param \Cake\ORM\Query $query The original query to modify
+ * @param array $options Options
+ * @return \Cake\ORM\Query
+ */
+ public function findTranslations(Query $query, array $options): Query
+ {
+ $locales = $options['locales'] ?? [];
+ $targetAlias = $this->getStrategy()->getTranslationTable()->getAlias();
+
+ return $query
+ ->contain([$targetAlias => function ($query) use ($locales, $targetAlias) {
+ /** @var \Cake\Datasource\QueryInterface $query */
+ if ($locales) {
+ $query->where(["$targetAlias.locale IN" => $locales]);
+ }
+
+ return $query;
+ }])
+ ->formatResults([$this->getStrategy(), 'groupTranslations'], $query::PREPEND);
+ }
+
+ /**
+ * Proxy method calls to strategy class instance.
+ *
+ * @param string $method Method name.
+ * @param array $args Method arguments.
+ * @return mixed
+ */
+ public function __call($method, $args)
+ {
+ return $this->strategy->{$method}(...$args);
+ }
+
+ /**
+ * Determine the reference name to use for a given table
+ *
+ * The reference name is usually derived from the class name of the table object
+ * (PostsTable -> Posts), however for autotable instances it is derived from
+ * the database table the object points at - or as a last resort, the alias
+ * of the autotable instance.
+ *
+ * @param \Cake\ORM\Table $table The table class to get a reference name for.
+ * @return string
+ */
+ protected function referenceName(Table $table): string
+ {
+ $name = namespaceSplit(get_class($table));
+ $name = substr(end($name), 0, -5);
+ if (empty($name)) {
+ $name = $table->getTable() ?: $table->getAlias();
+ $name = Inflector::camelize($name);
+ }
+
+ return $name;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Behavior/TreeBehavior.php b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TreeBehavior.php
new file mode 100644
index 000000000..af4f3282e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Behavior/TreeBehavior.php
@@ -0,0 +1,1024 @@
+ [
+ 'path' => 'findPath',
+ 'children' => 'findChildren',
+ 'treeList' => 'findTreeList',
+ ],
+ 'implementedMethods' => [
+ 'childCount' => 'childCount',
+ 'moveUp' => 'moveUp',
+ 'moveDown' => 'moveDown',
+ 'recover' => 'recover',
+ 'removeFromTree' => 'removeFromTree',
+ 'getLevel' => 'getLevel',
+ 'formatTreeList' => 'formatTreeList',
+ ],
+ 'parent' => 'parent_id',
+ 'left' => 'lft',
+ 'right' => 'rght',
+ 'scope' => null,
+ 'level' => null,
+ 'recoverOrder' => null,
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function initialize(array $config): void
+ {
+ $this->_config['leftField'] = new IdentifierExpression($this->_config['left']);
+ $this->_config['rightField'] = new IdentifierExpression($this->_config['right']);
+ }
+
+ /**
+ * Before save listener.
+ * Transparently manages setting the lft and rght fields if the parent field is
+ * included in the parameters to be saved.
+ *
+ * @param \Cake\Event\EventInterface $event The beforeSave event that was fired
+ * @param \Cake\Datasource\EntityInterface $entity the entity that is going to be saved
+ * @return void
+ * @throws \RuntimeException if the parent to set for the node is invalid
+ */
+ public function beforeSave(EventInterface $event, EntityInterface $entity)
+ {
+ $isNew = $entity->isNew();
+ $config = $this->getConfig();
+ $parent = $entity->get($config['parent']);
+ $primaryKey = $this->_getPrimaryKey();
+ $dirty = $entity->isDirty($config['parent']);
+ $level = $config['level'];
+
+ if ($parent && $entity->get($primaryKey) === $parent) {
+ throw new RuntimeException("Cannot set a node's parent as itself");
+ }
+
+ if ($isNew && $parent) {
+ $parentNode = $this->_getNode($parent);
+ $edge = $parentNode->get($config['right']);
+ $entity->set($config['left'], $edge);
+ $entity->set($config['right'], $edge + 1);
+ $this->_sync(2, '+', ">= {$edge}");
+
+ if ($level) {
+ $entity->set($level, $parentNode[$level] + 1);
+ }
+
+ return;
+ }
+
+ if ($isNew && !$parent) {
+ $edge = $this->_getMax();
+ $entity->set($config['left'], $edge + 1);
+ $entity->set($config['right'], $edge + 2);
+
+ if ($level) {
+ $entity->set($level, 0);
+ }
+
+ return;
+ }
+
+ if ($dirty && $parent) {
+ $this->_setParent($entity, $parent);
+
+ if ($level) {
+ $parentNode = $this->_getNode($parent);
+ $entity->set($level, $parentNode[$level] + 1);
+ }
+
+ return;
+ }
+
+ if ($dirty && !$parent) {
+ $this->_setAsRoot($entity);
+
+ if ($level) {
+ $entity->set($level, 0);
+ }
+ }
+ }
+
+ /**
+ * After save listener.
+ *
+ * Manages updating level of descendants of currently saved entity.
+ *
+ * @param \Cake\Event\EventInterface $event The afterSave event that was fired
+ * @param \Cake\Datasource\EntityInterface $entity the entity that is going to be saved
+ * @return void
+ */
+ public function afterSave(EventInterface $event, EntityInterface $entity)
+ {
+ if (!$this->_config['level'] || $entity->isNew()) {
+ return;
+ }
+
+ $this->_setChildrenLevel($entity);
+ }
+
+ /**
+ * Set level for descendants.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity whose descendants need to be updated.
+ * @return void
+ */
+ protected function _setChildrenLevel(EntityInterface $entity): void
+ {
+ $config = $this->getConfig();
+
+ if ($entity->get($config['left']) + 1 === $entity->get($config['right'])) {
+ return;
+ }
+
+ $primaryKey = $this->_getPrimaryKey();
+ $primaryKeyValue = $entity->get($primaryKey);
+ $depths = [$primaryKeyValue => $entity->get($config['level'])];
+
+ $children = $this->_table->find('children', [
+ 'for' => $primaryKeyValue,
+ 'fields' => [$this->_getPrimaryKey(), $config['parent'], $config['level']],
+ 'order' => $config['left'],
+ ]);
+
+ /** @var \Cake\Datasource\EntityInterface $node */
+ foreach ($children as $node) {
+ $parentIdValue = $node->get($config['parent']);
+ $depth = $depths[$parentIdValue] + 1;
+ $depths[$node->get($primaryKey)] = $depth;
+
+ $this->_table->updateAll(
+ [$config['level'] => $depth],
+ [$primaryKey => $node->get($primaryKey)]
+ );
+ }
+ }
+
+ /**
+ * Also deletes the nodes in the subtree of the entity to be delete
+ *
+ * @param \Cake\Event\EventInterface $event The beforeDelete event that was fired
+ * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
+ * @return void
+ */
+ public function beforeDelete(EventInterface $event, EntityInterface $entity)
+ {
+ $config = $this->getConfig();
+ $this->_ensureFields($entity);
+ $left = $entity->get($config['left']);
+ $right = $entity->get($config['right']);
+ $diff = $right - $left + 1;
+
+ if ($diff > 2) {
+ $query = $this->_scope($this->_table->query())
+ ->delete()
+ ->where(function ($exp) use ($config, $left, $right) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp
+ ->gte($config['leftField'], $left + 1)
+ ->lte($config['leftField'], $right - 1);
+ });
+ $statement = $query->execute();
+ $statement->closeCursor();
+ }
+
+ $this->_sync($diff, '-', "> {$right}");
+ }
+
+ /**
+ * Sets the correct left and right values for the passed entity so it can be
+ * updated to a new parent. It also makes the hole in the tree so the node
+ * move can be done without corrupting the structure.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to re-parent
+ * @param mixed $parent the id of the parent to set
+ * @return void
+ * @throws \RuntimeException if the parent to set to the entity is not valid
+ */
+ protected function _setParent(EntityInterface $entity, $parent): void
+ {
+ $config = $this->getConfig();
+ $parentNode = $this->_getNode($parent);
+ $this->_ensureFields($entity);
+ $parentLeft = $parentNode->get($config['left']);
+ $parentRight = $parentNode->get($config['right']);
+ $right = $entity->get($config['right']);
+ $left = $entity->get($config['left']);
+
+ if ($parentLeft > $left && $parentLeft < $right) {
+ throw new RuntimeException(sprintf(
+ 'Cannot use node "%s" as parent for entity "%s"',
+ $parent,
+ $entity->get($this->_getPrimaryKey())
+ ));
+ }
+
+ // Values for moving to the left
+ $diff = $right - $left + 1;
+ $targetLeft = $parentRight;
+ $targetRight = $diff + $parentRight - 1;
+ $min = $parentRight;
+ $max = $left - 1;
+
+ if ($left < $targetLeft) {
+ // Moving to the right
+ $targetLeft = $parentRight - $diff;
+ $targetRight = $parentRight - 1;
+ $min = $right + 1;
+ $max = $parentRight - 1;
+ $diff *= -1;
+ }
+
+ if ($right - $left > 1) {
+ // Correcting internal subtree
+ $internalLeft = $left + 1;
+ $internalRight = $right - 1;
+ $this->_sync($targetLeft - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}", true);
+ }
+
+ $this->_sync($diff, '+', "BETWEEN {$min} AND {$max}");
+
+ if ($right - $left > 1) {
+ $this->_unmarkInternalTree();
+ }
+
+ // Allocating new position
+ $entity->set($config['left'], $targetLeft);
+ $entity->set($config['right'], $targetRight);
+ }
+
+ /**
+ * Updates the left and right column for the passed entity so it can be set as
+ * a new root in the tree. It also modifies the ordering in the rest of the tree
+ * so the structure remains valid
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to set as a new root
+ * @return void
+ */
+ protected function _setAsRoot(EntityInterface $entity): void
+ {
+ $config = $this->getConfig();
+ $edge = $this->_getMax();
+ $this->_ensureFields($entity);
+ $right = $entity->get($config['right']);
+ $left = $entity->get($config['left']);
+ $diff = $right - $left;
+
+ if ($right - $left > 1) {
+ //Correcting internal subtree
+ $internalLeft = $left + 1;
+ $internalRight = $right - 1;
+ $this->_sync($edge - $diff - $left, '+', "BETWEEN {$internalLeft} AND {$internalRight}", true);
+ }
+
+ $this->_sync($diff + 1, '-', "BETWEEN {$right} AND {$edge}");
+
+ if ($right - $left > 1) {
+ $this->_unmarkInternalTree();
+ }
+
+ $entity->set($config['left'], $edge - $diff);
+ $entity->set($config['right'], $edge);
+ }
+
+ /**
+ * Helper method used to invert the sign of the left and right columns that are
+ * less than 0. They were set to negative values before so their absolute value
+ * wouldn't change while performing other tree transformations.
+ *
+ * @return void
+ */
+ protected function _unmarkInternalTree(): void
+ {
+ $config = $this->getConfig();
+ $this->_table->updateAll(
+ function ($exp) use ($config) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ $leftInverse = clone $exp;
+ $leftInverse->setConjunction('*')->add('-1');
+ $rightInverse = clone $leftInverse;
+
+ return $exp
+ ->eq($config['leftField'], $leftInverse->add($config['leftField']))
+ ->eq($config['rightField'], $rightInverse->add($config['rightField']));
+ },
+ function ($exp) use ($config) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp->lt($config['leftField'], 0);
+ }
+ );
+ }
+
+ /**
+ * Custom finder method which can be used to return the list of nodes from the root
+ * to a specific node in the tree. This custom finder requires that the key 'for'
+ * is passed in the options containing the id of the node to get its path for.
+ *
+ * @param \Cake\ORM\Query $query The constructed query to modify
+ * @param array $options the list of options for the query
+ * @return \Cake\ORM\Query
+ * @throws \InvalidArgumentException If the 'for' key is missing in options
+ */
+ public function findPath(Query $query, array $options): Query
+ {
+ if (empty($options['for'])) {
+ throw new InvalidArgumentException("The 'for' key is required for find('path')");
+ }
+
+ $config = $this->getConfig();
+ [$left, $right] = array_map(
+ function ($field) {
+ return $this->_table->aliasField($field);
+ },
+ [$config['left'], $config['right']]
+ );
+
+ $node = $this->_table->get($options['for'], ['fields' => [$left, $right]]);
+
+ return $this->_scope($query)
+ ->where([
+ "$left <=" => $node->get($config['left']),
+ "$right >=" => $node->get($config['right']),
+ ])
+ ->order([$left => 'ASC']);
+ }
+
+ /**
+ * Get the number of children nodes.
+ *
+ * @param \Cake\Datasource\EntityInterface $node The entity to count children for
+ * @param bool $direct whether to count all nodes in the subtree or just
+ * direct children
+ * @return int Number of children nodes.
+ */
+ public function childCount(EntityInterface $node, bool $direct = false): int
+ {
+ $config = $this->getConfig();
+ $parent = $this->_table->aliasField($config['parent']);
+
+ if ($direct) {
+ return $this->_scope($this->_table->find())
+ ->where([$parent => $node->get($this->_getPrimaryKey())])
+ ->count();
+ }
+
+ $this->_ensureFields($node);
+
+ return ($node->get($config['right']) - $node->get($config['left']) - 1) / 2;
+ }
+
+ /**
+ * Get the children nodes of the current model
+ *
+ * Available options are:
+ *
+ * - for: The id of the record to read.
+ * - direct: Boolean, whether to return only the direct (true), or all (false) children,
+ * defaults to false (all children).
+ *
+ * If the direct option is set to true, only the direct children are returned (based upon the parent_id field)
+ *
+ * @param \Cake\ORM\Query $query Query.
+ * @param array $options Array of options as described above
+ * @return \Cake\ORM\Query
+ * @throws \InvalidArgumentException When the 'for' key is not passed in $options
+ */
+ public function findChildren(Query $query, array $options): Query
+ {
+ $config = $this->getConfig();
+ $options += ['for' => null, 'direct' => false];
+ [$parent, $left, $right] = array_map(
+ function ($field) {
+ return $this->_table->aliasField($field);
+ },
+ [$config['parent'], $config['left'], $config['right']]
+ );
+
+ [$for, $direct] = [$options['for'], $options['direct']];
+
+ if (empty($for)) {
+ throw new InvalidArgumentException("The 'for' key is required for find('children')");
+ }
+
+ if ($query->clause('order') === null) {
+ $query->order([$left => 'ASC']);
+ }
+
+ if ($direct) {
+ return $this->_scope($query)->where([$parent => $for]);
+ }
+
+ $node = $this->_getNode($for);
+
+ return $this->_scope($query)
+ ->where([
+ "{$right} <" => $node->get($config['right']),
+ "{$left} >" => $node->get($config['left']),
+ ]);
+ }
+
+ /**
+ * Gets a representation of the elements in the tree as a flat list where the keys are
+ * the primary key for the table and the values are the display field for the table.
+ * Values are prefixed to visually indicate relative depth in the tree.
+ *
+ * ### Options
+ *
+ * - keyPath: A dot separated path to fetch the field to use for the array key, or a closure to
+ * return the key out of the provided row.
+ * - valuePath: A dot separated path to fetch the field to use for the array value, or a closure to
+ * return the value out of the provided row.
+ * - spacer: A string to be used as prefix for denoting the depth in the tree for each item
+ *
+ * @param \Cake\ORM\Query $query Query.
+ * @param array $options Array of options as described above.
+ * @return \Cake\ORM\Query
+ */
+ public function findTreeList(Query $query, array $options): Query
+ {
+ $left = $this->_table->aliasField($this->getConfig('left'));
+
+ $results = $this->_scope($query)
+ ->find('threaded', [
+ 'parentField' => $this->getConfig('parent'),
+ 'order' => [$left => 'ASC'],
+ ]);
+
+ return $this->formatTreeList($results, $options);
+ }
+
+ /**
+ * Formats query as a flat list where the keys are the primary key for the table
+ * and the values are the display field for the table. Values are prefixed to visually
+ * indicate relative depth in the tree.
+ *
+ * ### Options
+ *
+ * - keyPath: A dot separated path to the field that will be the result array key, or a closure to
+ * return the key from the provided row.
+ * - valuePath: A dot separated path to the field that is the array's value, or a closure to
+ * return the value from the provided row.
+ * - spacer: A string to be used as prefix for denoting the depth in the tree for each item.
+ *
+ * @param \Cake\ORM\Query $query The query object to format.
+ * @param array $options Array of options as described above.
+ * @return \Cake\ORM\Query Augmented query.
+ */
+ public function formatTreeList(Query $query, array $options = []): Query
+ {
+ return $query->formatResults(function (CollectionInterface $results) use ($options) {
+ $options += [
+ 'keyPath' => $this->_getPrimaryKey(),
+ 'valuePath' => $this->_table->getDisplayField(),
+ 'spacer' => '_',
+ ];
+
+ /** @var \Cake\Collection\Iterator\TreeIterator $nested */
+ $nested = $results->listNested();
+
+ return $nested->printer($options['valuePath'], $options['keyPath'], $options['spacer']);
+ });
+ }
+
+ /**
+ * Removes the current node from the tree, by positioning it as a new root
+ * and re-parents all children up one level.
+ *
+ * Note that the node will not be deleted just moved away from its current position
+ * without moving its children with it.
+ *
+ * @param \Cake\Datasource\EntityInterface $node The node to remove from the tree
+ * @return \Cake\Datasource\EntityInterface|false the node after being removed from the tree or
+ * false on error
+ */
+ public function removeFromTree(EntityInterface $node)
+ {
+ return $this->_table->getConnection()->transactional(function () use ($node) {
+ $this->_ensureFields($node);
+
+ return $this->_removeFromTree($node);
+ });
+ }
+
+ /**
+ * Helper function containing the actual code for removeFromTree
+ *
+ * @param \Cake\Datasource\EntityInterface $node The node to remove from the tree
+ * @return \Cake\Datasource\EntityInterface|false the node after being removed from the tree or
+ * false on error
+ */
+ protected function _removeFromTree(EntityInterface $node)
+ {
+ $config = $this->getConfig();
+ $left = $node->get($config['left']);
+ $right = $node->get($config['right']);
+ $parent = $node->get($config['parent']);
+
+ $node->set($config['parent'], null);
+
+ if ($right - $left === 1) {
+ return $this->_table->save($node);
+ }
+
+ $primary = $this->_getPrimaryKey();
+ $this->_table->updateAll(
+ [$config['parent'] => $parent],
+ [$config['parent'] => $node->get($primary)]
+ );
+ $this->_sync(1, '-', 'BETWEEN ' . ($left + 1) . ' AND ' . ($right - 1));
+ $this->_sync(2, '-', "> {$right}");
+ $edge = $this->_getMax();
+ $node->set($config['left'], $edge + 1);
+ $node->set($config['right'], $edge + 2);
+ $fields = [$config['parent'], $config['left'], $config['right']];
+
+ $this->_table->updateAll($node->extract($fields), [$primary => $node->get($primary)]);
+
+ foreach ($fields as $field) {
+ $node->setDirty($field, false);
+ }
+
+ return $node;
+ }
+
+ /**
+ * Reorders the node without changing its parent.
+ *
+ * If the node is the first child, or is a top level node with no previous node
+ * this method will return false
+ *
+ * @param \Cake\Datasource\EntityInterface $node The node to move
+ * @param int|true $number How many places to move the node, or true to move to first position
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found
+ * @return \Cake\Datasource\EntityInterface|false $node The node after being moved or false on failure
+ */
+ public function moveUp(EntityInterface $node, $number = 1)
+ {
+ if ($number < 1) {
+ return false;
+ }
+
+ return $this->_table->getConnection()->transactional(function () use ($node, $number) {
+ $this->_ensureFields($node);
+
+ return $this->_moveUp($node, $number);
+ });
+ }
+
+ /**
+ * Helper function used with the actual code for moveUp
+ *
+ * @param \Cake\Datasource\EntityInterface $node The node to move
+ * @param int|true $number How many places to move the node, or true to move to first position
+ * @return \Cake\Datasource\EntityInterface $node The node after being moved or false on failure
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found
+ */
+ protected function _moveUp(EntityInterface $node, $number): EntityInterface
+ {
+ $config = $this->getConfig();
+ [$parent, $left, $right] = [$config['parent'], $config['left'], $config['right']];
+ [$nodeParent, $nodeLeft, $nodeRight] = array_values($node->extract([$parent, $left, $right]));
+
+ $targetNode = null;
+ if ($number !== true) {
+ /** @var \Cake\Datasource\EntityInterface|null $targetNode */
+ $targetNode = $this->_scope($this->_table->find())
+ ->select([$left, $right])
+ ->where(["$parent IS" => $nodeParent])
+ ->where(function ($exp) use ($config, $nodeLeft) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp->lt($config['rightField'], $nodeLeft);
+ })
+ ->orderDesc($config['leftField'])
+ ->offset($number - 1)
+ ->limit(1)
+ ->first();
+ }
+ if (!$targetNode) {
+ /** @var \Cake\Datasource\EntityInterface|null $targetNode */
+ $targetNode = $this->_scope($this->_table->find())
+ ->select([$left, $right])
+ ->where(["$parent IS" => $nodeParent])
+ ->where(function ($exp) use ($config, $nodeLeft) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp->lt($config['rightField'], $nodeLeft);
+ })
+ ->orderAsc($config['leftField'])
+ ->limit(1)
+ ->first();
+
+ if (!$targetNode) {
+ return $node;
+ }
+ }
+
+ [$targetLeft] = array_values($targetNode->extract([$left, $right]));
+ $edge = $this->_getMax();
+ $leftBoundary = $targetLeft;
+ $rightBoundary = $nodeLeft - 1;
+
+ $nodeToEdge = $edge - $nodeLeft + 1;
+ $shift = $nodeRight - $nodeLeft + 1;
+ $nodeToHole = $edge - $leftBoundary + 1;
+ $this->_sync($nodeToEdge, '+', "BETWEEN {$nodeLeft} AND {$nodeRight}");
+ $this->_sync($shift, '+', "BETWEEN {$leftBoundary} AND {$rightBoundary}");
+ $this->_sync($nodeToHole, '-', "> {$edge}");
+
+ $node->set($left, $targetLeft);
+ $node->set($right, $targetLeft + $nodeRight - $nodeLeft);
+
+ $node->setDirty($left, false);
+ $node->setDirty($right, false);
+
+ return $node;
+ }
+
+ /**
+ * Reorders the node without changing the parent.
+ *
+ * If the node is the last child, or is a top level node with no subsequent node
+ * this method will return false
+ *
+ * @param \Cake\Datasource\EntityInterface $node The node to move
+ * @param int|true $number How many places to move the node or true to move to last position
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found
+ * @return \Cake\Datasource\EntityInterface|false the entity after being moved or false on failure
+ */
+ public function moveDown(EntityInterface $node, $number = 1)
+ {
+ if ($number < 1) {
+ return false;
+ }
+
+ return $this->_table->getConnection()->transactional(function () use ($node, $number) {
+ $this->_ensureFields($node);
+
+ return $this->_moveDown($node, $number);
+ });
+ }
+
+ /**
+ * Helper function used with the actual code for moveDown
+ *
+ * @param \Cake\Datasource\EntityInterface $node The node to move
+ * @param int|true $number How many places to move the node, or true to move to last position
+ * @return \Cake\Datasource\EntityInterface $node The node after being moved or false on failure
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found
+ */
+ protected function _moveDown(EntityInterface $node, $number): EntityInterface
+ {
+ $config = $this->getConfig();
+ [$parent, $left, $right] = [$config['parent'], $config['left'], $config['right']];
+ [$nodeParent, $nodeLeft, $nodeRight] = array_values($node->extract([$parent, $left, $right]));
+
+ $targetNode = null;
+ if ($number !== true) {
+ /** @var \Cake\Datasource\EntityInterface|null $targetNode */
+ $targetNode = $this->_scope($this->_table->find())
+ ->select([$left, $right])
+ ->where(["$parent IS" => $nodeParent])
+ ->where(function ($exp) use ($config, $nodeRight) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp->gt($config['leftField'], $nodeRight);
+ })
+ ->orderAsc($config['leftField'])
+ ->offset($number - 1)
+ ->limit(1)
+ ->first();
+ }
+ if (!$targetNode) {
+ /** @var \Cake\Datasource\EntityInterface|null $targetNode */
+ $targetNode = $this->_scope($this->_table->find())
+ ->select([$left, $right])
+ ->where(["$parent IS" => $nodeParent])
+ ->where(function ($exp) use ($config, $nodeRight) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp->gt($config['leftField'], $nodeRight);
+ })
+ ->orderDesc($config['leftField'])
+ ->limit(1)
+ ->first();
+
+ if (!$targetNode) {
+ return $node;
+ }
+ }
+
+ [, $targetRight] = array_values($targetNode->extract([$left, $right]));
+ $edge = $this->_getMax();
+ $leftBoundary = $nodeRight + 1;
+ $rightBoundary = $targetRight;
+
+ $nodeToEdge = $edge - $nodeLeft + 1;
+ $shift = $nodeRight - $nodeLeft + 1;
+ $nodeToHole = $edge - $rightBoundary + $shift;
+ $this->_sync($nodeToEdge, '+', "BETWEEN {$nodeLeft} AND {$nodeRight}");
+ $this->_sync($shift, '-', "BETWEEN {$leftBoundary} AND {$rightBoundary}");
+ $this->_sync($nodeToHole, '-', "> {$edge}");
+
+ $node->set($left, $targetRight - ($nodeRight - $nodeLeft));
+ $node->set($right, $targetRight);
+
+ $node->setDirty($left, false);
+ $node->setDirty($right, false);
+
+ return $node;
+ }
+
+ /**
+ * Returns a single node from the tree from its primary key
+ *
+ * @param mixed $id Record id.
+ * @return \Cake\Datasource\EntityInterface
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found
+ * @psalm-suppress InvalidReturnType
+ */
+ protected function _getNode($id): EntityInterface
+ {
+ $config = $this->getConfig();
+ [$parent, $left, $right] = [$config['parent'], $config['left'], $config['right']];
+ $primaryKey = $this->_getPrimaryKey();
+ $fields = [$parent, $left, $right];
+ if ($config['level']) {
+ $fields[] = $config['level'];
+ }
+
+ $node = $this->_scope($this->_table->find())
+ ->select($fields)
+ ->where([$this->_table->aliasField($primaryKey) => $id])
+ ->first();
+
+ if (!$node) {
+ throw new RecordNotFoundException("Node \"{$id}\" was not found in the tree.");
+ }
+
+ /** @psalm-suppress InvalidReturnStatement */
+ return $node;
+ }
+
+ /**
+ * Recovers the lft and right column values out of the hierarchy defined by the
+ * parent column.
+ *
+ * @return void
+ */
+ public function recover(): void
+ {
+ $this->_table->getConnection()->transactional(function (): void {
+ $this->_recoverTree();
+ });
+ }
+
+ /**
+ * Recursive method used to recover a single level of the tree
+ *
+ * @param int $counter The Last left column value that was assigned
+ * @param mixed $parentId the parent id of the level to be recovered
+ * @param int $level Node level
+ * @return int The next value to use for the left column
+ */
+ protected function _recoverTree(int $counter = 0, $parentId = null, $level = -1): int
+ {
+ $config = $this->getConfig();
+ [$parent, $left, $right] = [$config['parent'], $config['left'], $config['right']];
+ $primaryKey = $this->_getPrimaryKey();
+ $aliasedPrimaryKey = $this->_table->aliasField($primaryKey);
+ $order = $config['recoverOrder'] ?: $aliasedPrimaryKey;
+
+ $query = $this->_scope($this->_table->query())
+ ->select([$aliasedPrimaryKey])
+ ->where([$this->_table->aliasField($parent) . ' IS' => $parentId])
+ ->order($order)
+ ->disableHydration();
+
+ $leftCounter = $counter;
+ $nextLevel = $level + 1;
+ foreach ($query as $row) {
+ $counter++;
+ $counter = $this->_recoverTree($counter, $row[$primaryKey], $nextLevel);
+ }
+
+ if ($parentId === null) {
+ return $counter;
+ }
+
+ $fields = [$left => $leftCounter, $right => $counter + 1];
+ if ($config['level']) {
+ $fields[$config['level']] = $level;
+ }
+
+ $this->_table->updateAll(
+ $fields,
+ [$primaryKey => $parentId]
+ );
+
+ return $counter + 1;
+ }
+
+ /**
+ * Returns the maximum index value in the table.
+ *
+ * @return int
+ */
+ protected function _getMax(): int
+ {
+ $field = $this->_config['right'];
+ $rightField = $this->_config['rightField'];
+ $edge = $this->_scope($this->_table->find())
+ ->select([$field])
+ ->orderDesc($rightField)
+ ->first();
+
+ if ($edge === null || empty($edge[$field])) {
+ return 0;
+ }
+
+ return $edge[$field];
+ }
+
+ /**
+ * Auxiliary function used to automatically alter the value of both the left and
+ * right columns by a certain amount that match the passed conditions
+ *
+ * @param int $shift the value to use for operating the left and right columns
+ * @param string $dir The operator to use for shifting the value (+/-)
+ * @param string $conditions a SQL snipped to be used for comparing left or right
+ * against it.
+ * @param bool $mark whether to mark the updated values so that they can not be
+ * modified by future calls to this function.
+ * @return void
+ */
+ protected function _sync(int $shift, string $dir, string $conditions, bool $mark = false): void
+ {
+ $config = $this->_config;
+
+ foreach ([$config['leftField'], $config['rightField']] as $field) {
+ $query = $this->_scope($this->_table->query());
+ $exp = $query->newExpr();
+
+ $movement = clone $exp;
+ $movement->add($field)->add((string)$shift)->setConjunction($dir);
+
+ $inverse = clone $exp;
+ $movement = $mark ?
+ $inverse->add($movement)->setConjunction('*')->add('-1') :
+ $movement;
+
+ $where = clone $exp;
+ $where->add($field)->add($conditions)->setConjunction('');
+
+ $query->update()
+ ->set($exp->eq($field, $movement))
+ ->where($where);
+
+ $query->execute()->closeCursor();
+ }
+ }
+
+ /**
+ * Alters the passed query so that it only returns scoped records as defined
+ * in the tree configuration.
+ *
+ * @param \Cake\ORM\Query $query the Query to modify
+ * @return \Cake\ORM\Query
+ */
+ protected function _scope(Query $query): Query
+ {
+ $scope = $this->getConfig('scope');
+
+ if (is_array($scope)) {
+ return $query->where($scope);
+ }
+ if (is_callable($scope)) {
+ return $scope($query);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Ensures that the provided entity contains non-empty values for the left and
+ * right fields
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to ensure fields for
+ * @return void
+ */
+ protected function _ensureFields(EntityInterface $entity): void
+ {
+ $config = $this->getConfig();
+ $fields = [$config['left'], $config['right']];
+ $values = array_filter($entity->extract($fields));
+ if (count($values) === count($fields)) {
+ return;
+ }
+
+ $fresh = $this->_table->get($entity->get($this->_getPrimaryKey()), $fields);
+ $entity->set($fresh->extract($fields), ['guard' => false]);
+
+ foreach ($fields as $field) {
+ $entity->setDirty($field, false);
+ }
+ }
+
+ /**
+ * Returns a single string value representing the primary key of the attached table
+ *
+ * @return string
+ */
+ protected function _getPrimaryKey(): string
+ {
+ if (!$this->_primaryKey) {
+ $primaryKey = (array)$this->_table->getPrimaryKey();
+ $this->_primaryKey = $primaryKey[0];
+ }
+
+ return $this->_primaryKey;
+ }
+
+ /**
+ * Returns the depth level of a node in the tree.
+ *
+ * @param int|string|\Cake\Datasource\EntityInterface $entity The entity or primary key get the level of.
+ * @return int|false Integer of the level or false if the node does not exist.
+ */
+ public function getLevel($entity)
+ {
+ $primaryKey = $this->_getPrimaryKey();
+ $id = $entity;
+ if ($entity instanceof EntityInterface) {
+ $id = $entity->get($primaryKey);
+ }
+ $config = $this->getConfig();
+ $entity = $this->_table->find('all')
+ ->select([$config['left'], $config['right']])
+ ->where([$primaryKey => $id])
+ ->first();
+
+ if ($entity === null) {
+ return false;
+ }
+
+ $query = $this->_table->find('all')->where([
+ $config['left'] . ' <' => $entity[$config['left']],
+ $config['right'] . ' >' => $entity[$config['right']],
+ ]);
+
+ return $this->_scope($query)->count();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php b/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php
new file mode 100644
index 000000000..7f7b2c2c1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/BehaviorRegistry.php
@@ -0,0 +1,283 @@
+
+ */
+class BehaviorRegistry extends ObjectRegistry implements EventDispatcherInterface
+{
+ use EventDispatcherTrait;
+
+ /**
+ * The table using this registry.
+ *
+ * @var \Cake\ORM\Table
+ */
+ protected $_table;
+
+ /**
+ * Method mappings.
+ *
+ * @var array
+ */
+ protected $_methodMap = [];
+
+ /**
+ * Finder method mappings.
+ *
+ * @var array
+ */
+ protected $_finderMap = [];
+
+ /**
+ * Constructor
+ *
+ * @param \Cake\ORM\Table|null $table The table this registry is attached to.
+ */
+ public function __construct(?Table $table = null)
+ {
+ if ($table !== null) {
+ $this->setTable($table);
+ }
+ }
+
+ /**
+ * Attaches a table instance to this registry.
+ *
+ * @param \Cake\ORM\Table $table The table this registry is attached to.
+ * @return void
+ */
+ public function setTable(Table $table): void
+ {
+ $this->_table = $table;
+ $this->setEventManager($table->getEventManager());
+ }
+
+ /**
+ * Resolve a behavior classname.
+ *
+ * @param string $class Partial classname to resolve.
+ * @return string|null Either the correct classname or null.
+ * @psalm-return class-string|null
+ */
+ public static function className(string $class): ?string
+ {
+ return App::className($class, 'Model/Behavior', 'Behavior')
+ ?: App::className($class, 'ORM/Behavior', 'Behavior');
+ }
+
+ /**
+ * Resolve a behavior classname.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ *
+ * @param string $class Partial classname to resolve.
+ * @return string|null Either the correct class name or null.
+ * @psalm-return class-string|null
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ return static::className($class);
+ }
+
+ /**
+ * Throws an exception when a behavior is missing.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ * and Cake\Core\ObjectRegistry::unload()
+ *
+ * @param string $class The classname that is missing.
+ * @param string|null $plugin The plugin the behavior is missing in.
+ * @return void
+ * @throws \Cake\ORM\Exception\MissingBehaviorException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ throw new MissingBehaviorException([
+ 'class' => $class . 'Behavior',
+ 'plugin' => $plugin,
+ ]);
+ }
+
+ /**
+ * Create the behavior instance.
+ *
+ * Part of the template method for Cake\Core\ObjectRegistry::load()
+ * Enabled behaviors will be registered with the event manager.
+ *
+ * @param string $class The classname that is missing.
+ * @param string $alias The alias of the object.
+ * @param array $config An array of config to use for the behavior.
+ * @return \Cake\ORM\Behavior The constructed behavior class.
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ protected function _create($class, string $alias, array $config): Behavior
+ {
+ /** @var \Cake\ORM\Behavior $instance */
+ $instance = new $class($this->_table, $config);
+ $enable = $config['enabled'] ?? true;
+ if ($enable) {
+ $this->getEventManager()->on($instance);
+ }
+ $methods = $this->_getMethods($instance, $class, $alias);
+ $this->_methodMap += $methods['methods'];
+ $this->_finderMap += $methods['finders'];
+
+ return $instance;
+ }
+
+ /**
+ * Get the behavior methods and ensure there are no duplicates.
+ *
+ * Use the implementedEvents() method to exclude callback methods.
+ * Methods starting with `_` will be ignored, as will methods
+ * declared on Cake\ORM\Behavior
+ *
+ * @param \Cake\ORM\Behavior $instance The behavior to get methods from.
+ * @param string $class The classname that is missing.
+ * @param string $alias The alias of the object.
+ * @return array A list of implemented finders and methods.
+ * @throws \LogicException when duplicate methods are connected.
+ */
+ protected function _getMethods(Behavior $instance, string $class, string $alias): array
+ {
+ $finders = array_change_key_case($instance->implementedFinders());
+ $methods = array_change_key_case($instance->implementedMethods());
+
+ foreach ($finders as $finder => $methodName) {
+ if (isset($this->_finderMap[$finder]) && $this->has($this->_finderMap[$finder][0])) {
+ $duplicate = $this->_finderMap[$finder];
+ $error = sprintf(
+ '%s contains duplicate finder "%s" which is already provided by "%s"',
+ $class,
+ $finder,
+ $duplicate[0]
+ );
+ throw new LogicException($error);
+ }
+ $finders[$finder] = [$alias, $methodName];
+ }
+
+ foreach ($methods as $method => $methodName) {
+ if (isset($this->_methodMap[$method]) && $this->has($this->_methodMap[$method][0])) {
+ $duplicate = $this->_methodMap[$method];
+ $error = sprintf(
+ '%s contains duplicate method "%s" which is already provided by "%s"',
+ $class,
+ $method,
+ $duplicate[0]
+ );
+ throw new LogicException($error);
+ }
+ $methods[$method] = [$alias, $methodName];
+ }
+
+ return compact('methods', 'finders');
+ }
+
+ /**
+ * Check if any loaded behavior implements a method.
+ *
+ * Will return true if any behavior provides a public non-finder method
+ * with the chosen name.
+ *
+ * @param string $method The method to check for.
+ * @return bool
+ */
+ public function hasMethod(string $method): bool
+ {
+ $method = strtolower($method);
+
+ return isset($this->_methodMap[$method]);
+ }
+
+ /**
+ * Check if any loaded behavior implements the named finder.
+ *
+ * Will return true if any behavior provides a public method with
+ * the chosen name.
+ *
+ * @param string $method The method to check for.
+ * @return bool
+ */
+ public function hasFinder(string $method): bool
+ {
+ $method = strtolower($method);
+
+ return isset($this->_finderMap[$method]);
+ }
+
+ /**
+ * Invoke a method on a behavior.
+ *
+ * @param string $method The method to invoke.
+ * @param array $args The arguments you want to invoke the method with.
+ * @return mixed The return value depends on the underlying behavior method.
+ * @throws \BadMethodCallException When the method is unknown.
+ */
+ public function call(string $method, array $args = [])
+ {
+ $method = strtolower($method);
+ if ($this->hasMethod($method) && $this->has($this->_methodMap[$method][0])) {
+ [$behavior, $callMethod] = $this->_methodMap[$method];
+
+ return $this->_loaded[$behavior]->{$callMethod}(...$args);
+ }
+
+ throw new BadMethodCallException(
+ sprintf('Cannot call "%s" it does not belong to any attached behavior.', $method)
+ );
+ }
+
+ /**
+ * Invoke a finder on a behavior.
+ *
+ * @param string $type The finder type to invoke.
+ * @param array $args The arguments you want to invoke the method with.
+ * @return \Cake\ORM\Query The return value depends on the underlying behavior method.
+ * @throws \BadMethodCallException When the method is unknown.
+ */
+ public function callFinder(string $type, array $args = []): Query
+ {
+ $type = strtolower($type);
+
+ if ($this->hasFinder($type) && $this->has($this->_finderMap[$type][0])) {
+ [$behavior, $callMethod] = $this->_finderMap[$type];
+ $callable = [$this->_loaded[$behavior], $callMethod];
+
+ return $callable(...$args);
+ }
+
+ throw new BadMethodCallException(
+ sprintf('Cannot call finder "%s" it does not belong to any attached behavior.', $type)
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/EagerLoadable.php b/app/vendor/cakephp/cakephp/src/ORM/EagerLoadable.php
new file mode 100644
index 000000000..c6e98c881
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/EagerLoadable.php
@@ -0,0 +1,326 @@
+author->company->country
+ * ```
+ *
+ * The property path of `country` will be `author.company`
+ *
+ * @var string|null
+ */
+ protected $_propertyPath;
+
+ /**
+ * Whether or not this level can be fetched using a join.
+ *
+ * @var bool
+ */
+ protected $_canBeJoined = false;
+
+ /**
+ * Whether or not this level was meant for a "matching" fetch
+ * operation
+ *
+ * @var bool|null
+ */
+ protected $_forMatching;
+
+ /**
+ * The property name where the association result should be nested
+ * in the result.
+ *
+ * For example, in the following nested property:
+ *
+ * ```
+ * $article->author->company->country
+ * ```
+ *
+ * The target property of `country` will be just `country`
+ *
+ * @var string|null
+ */
+ protected $_targetProperty;
+
+ /**
+ * Constructor. The $config parameter accepts the following array
+ * keys:
+ *
+ * - associations
+ * - instance
+ * - config
+ * - canBeJoined
+ * - aliasPath
+ * - propertyPath
+ * - forMatching
+ * - targetProperty
+ *
+ * The keys maps to the settable properties in this class.
+ *
+ * @param string $name The Association name.
+ * @param array $config The list of properties to set.
+ */
+ public function __construct(string $name, array $config = [])
+ {
+ $this->_name = $name;
+ $allowed = [
+ 'associations', 'instance', 'config', 'canBeJoined',
+ 'aliasPath', 'propertyPath', 'forMatching', 'targetProperty',
+ ];
+ foreach ($allowed as $property) {
+ if (isset($config[$property])) {
+ $this->{'_' . $property} = $config[$property];
+ }
+ }
+ }
+
+ /**
+ * Adds a new association to be loaded from this level.
+ *
+ * @param string $name The association name.
+ * @param \Cake\ORM\EagerLoadable $association The association to load.
+ * @return void
+ */
+ public function addAssociation(string $name, EagerLoadable $association): void
+ {
+ $this->_associations[$name] = $association;
+ }
+
+ /**
+ * Returns the Association class instance to use for loading the records.
+ *
+ * @return \Cake\ORM\EagerLoadable[]
+ */
+ public function associations(): array
+ {
+ return $this->_associations;
+ }
+
+ /**
+ * Gets the Association class instance to use for loading the records.
+ *
+ * @return \Cake\ORM\Association
+ * @throws \RuntimeException
+ */
+ public function instance(): Association
+ {
+ if ($this->_instance === null) {
+ throw new \RuntimeException('No instance set.');
+ }
+
+ return $this->_instance;
+ }
+
+ /**
+ * Gets a dot separated string representing the path of associations
+ * that should be followed to fetch this level.
+ *
+ * @return string
+ */
+ public function aliasPath(): string
+ {
+ return $this->_aliasPath;
+ }
+
+ /**
+ * Gets a dot separated string representing the path of entity properties
+ * in which results for this level should be placed.
+ *
+ * For example, in the following nested property:
+ *
+ * ```
+ * $article->author->company->country
+ * ```
+ *
+ * The property path of `country` will be `author.company`
+ *
+ * @return string|null
+ */
+ public function propertyPath(): ?string
+ {
+ return $this->_propertyPath;
+ }
+
+ /**
+ * Sets whether or not this level can be fetched using a join.
+ *
+ * @param bool $possible The value to set.
+ * @return $this
+ */
+ public function setCanBeJoined(bool $possible)
+ {
+ $this->_canBeJoined = $possible;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether or not this level can be fetched using a join.
+ *
+ * @return bool
+ */
+ public function canBeJoined(): bool
+ {
+ return $this->_canBeJoined;
+ }
+
+ /**
+ * Sets the list of options to pass to the association object for loading
+ * the records.
+ *
+ * @param array $config The value to set.
+ * @return $this
+ */
+ public function setConfig(array $config)
+ {
+ $this->_config = $config;
+
+ return $this;
+ }
+
+ /**
+ * Gets the list of options to pass to the association object for loading
+ * the records.
+ *
+ * @return array
+ */
+ public function getConfig(): array
+ {
+ return $this->_config;
+ }
+
+ /**
+ * Gets whether or not this level was meant for a
+ * "matching" fetch operation.
+ *
+ * @return bool|null
+ */
+ public function forMatching(): ?bool
+ {
+ return $this->_forMatching;
+ }
+
+ /**
+ * The property name where the result of this association
+ * should be nested at the end.
+ *
+ * For example, in the following nested property:
+ *
+ * ```
+ * $article->author->company->country
+ * ```
+ *
+ * The target property of `country` will be just `country`
+ *
+ * @return string|null
+ */
+ public function targetProperty(): ?string
+ {
+ return $this->_targetProperty;
+ }
+
+ /**
+ * Returns a representation of this object that can be passed to
+ * Cake\ORM\EagerLoader::contain()
+ *
+ * @return array
+ */
+ public function asContainArray(): array
+ {
+ $associations = [];
+ foreach ($this->_associations as $assoc) {
+ $associations += $assoc->asContainArray();
+ }
+ $config = $this->_config;
+ if ($this->_forMatching !== null) {
+ $config = ['matching' => $this->_forMatching] + $config;
+ }
+
+ return [
+ $this->_name => [
+ 'associations' => $associations,
+ 'config' => $config,
+ ],
+ ];
+ }
+
+ /**
+ * Handles cloning eager loadables.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ foreach ($this->_associations as $i => $association) {
+ $this->_associations[$i] = clone $association;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/EagerLoader.php b/app/vendor/cakephp/cakephp/src/ORM/EagerLoader.php
new file mode 100644
index 000000000..671acfa3f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/EagerLoader.php
@@ -0,0 +1,875 @@
+ 1,
+ 'foreignKey' => 1,
+ 'conditions' => 1,
+ 'fields' => 1,
+ 'sort' => 1,
+ 'matching' => 1,
+ 'queryBuilder' => 1,
+ 'finder' => 1,
+ 'joinType' => 1,
+ 'strategy' => 1,
+ 'negateMatch' => 1,
+ ];
+
+ /**
+ * A list of associations that should be loaded with a separate query
+ *
+ * @var \Cake\ORM\EagerLoadable[]
+ */
+ protected $_loadExternal = [];
+
+ /**
+ * Contains a list of the association names that are to be eagerly loaded
+ *
+ * @var array
+ */
+ protected $_aliasList = [];
+
+ /**
+ * Another EagerLoader instance that will be used for 'matching' associations.
+ *
+ * @var \Cake\ORM\EagerLoader|null
+ */
+ protected $_matching;
+
+ /**
+ * A map of table aliases pointing to the association objects they represent
+ * for the query.
+ *
+ * @var array
+ */
+ protected $_joinsMap = [];
+
+ /**
+ * Controls whether or not fields from associated tables
+ * will be eagerly loaded. When set to false, no fields will
+ * be loaded from associations.
+ *
+ * @var bool
+ */
+ protected $_autoFields = true;
+
+ /**
+ * Sets the list of associations that should be eagerly loaded along for a
+ * specific table using when a query is provided. The list of associated tables
+ * passed to this method must have been previously set as associations using the
+ * Table API.
+ *
+ * Associations can be arbitrarily nested using dot notation or nested arrays,
+ * this allows this object to calculate joins or any additional queries that
+ * must be executed to bring the required associated data.
+ *
+ * Accepted options per passed association:
+ *
+ * - foreignKey: Used to set a different field to match both tables, if set to false
+ * no join conditions will be generated automatically
+ * - fields: An array with the fields that should be fetched from the association
+ * - queryBuilder: Equivalent to passing a callable instead of an options array
+ * - matching: Whether to inform the association class that it should filter the
+ * main query by the results fetched by that class.
+ * - joinType: For joinable associations, the SQL join type to use.
+ * - strategy: The loading strategy to use (join, select, subquery)
+ *
+ * @param array|string $associations list of table aliases to be queried.
+ * When this method is called multiple times it will merge previous list with
+ * the new one.
+ * @param callable|null $queryBuilder The query builder callable
+ * @return array Containments.
+ * @throws \InvalidArgumentException When using $queryBuilder with an array of $associations
+ */
+ public function contain($associations, ?callable $queryBuilder = null): array
+ {
+ if ($queryBuilder) {
+ if (!is_string($associations)) {
+ throw new InvalidArgumentException(
+ sprintf('Cannot set containments. To use $queryBuilder, $associations must be a string')
+ );
+ }
+
+ $associations = [
+ $associations => [
+ 'queryBuilder' => $queryBuilder,
+ ],
+ ];
+ }
+
+ $associations = (array)$associations;
+ $associations = $this->_reformatContain($associations, $this->_containments);
+ $this->_normalized = null;
+ $this->_loadExternal = [];
+ $this->_aliasList = [];
+
+ return $this->_containments = $associations;
+ }
+
+ /**
+ * Gets the list of associations that should be eagerly loaded along for a
+ * specific table using when a query is provided. The list of associated tables
+ * passed to this method must have been previously set as associations using the
+ * Table API.
+ *
+ * @return array Containments.
+ */
+ public function getContain(): array
+ {
+ return $this->_containments;
+ }
+
+ /**
+ * Remove any existing non-matching based containments.
+ *
+ * This will reset/clear out any contained associations that were not
+ * added via matching().
+ *
+ * @return void
+ */
+ public function clearContain(): void
+ {
+ $this->_containments = [];
+ $this->_normalized = null;
+ $this->_loadExternal = [];
+ $this->_aliasList = [];
+ }
+
+ /**
+ * Sets whether or not contained associations will load fields automatically.
+ *
+ * @param bool $enable The value to set.
+ * @return $this
+ */
+ public function enableAutoFields(bool $enable = true)
+ {
+ $this->_autoFields = $enable;
+
+ return $this;
+ }
+
+ /**
+ * Disable auto loading fields of contained associations.
+ *
+ * @return $this
+ */
+ public function disableAutoFields()
+ {
+ $this->_autoFields = false;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether or not contained associations will load fields automatically.
+ *
+ * @return bool The current value.
+ */
+ public function isAutoFieldsEnabled(): bool
+ {
+ return $this->_autoFields;
+ }
+
+ /**
+ * Adds a new association to the list that will be used to filter the results of
+ * any given query based on the results of finding records for that association.
+ * You can pass a dot separated path of associations to this method as its first
+ * parameter, this will translate in setting all those associations with the
+ * `matching` option.
+ *
+ * ### Options
+ *
+ * - 'joinType': INNER, OUTER, ...
+ * - 'fields': Fields to contain
+ *
+ * @param string $assoc A single association or a dot separated path of associations.
+ * @param callable|null $builder the callback function to be used for setting extra
+ * options to the filtering query
+ * @param array $options Extra options for the association matching.
+ * @return $this
+ */
+ public function setMatching(string $assoc, ?callable $builder = null, array $options = [])
+ {
+ if ($this->_matching === null) {
+ $this->_matching = new static();
+ }
+
+ if (!isset($options['joinType'])) {
+ $options['joinType'] = Query::JOIN_TYPE_INNER;
+ }
+
+ $assocs = explode('.', $assoc);
+ $last = array_pop($assocs);
+ $containments = [];
+ $pointer = &$containments;
+ $opts = ['matching' => true] + $options;
+ /** @psalm-suppress InvalidArrayOffset */
+ unset($opts['negateMatch']);
+
+ foreach ($assocs as $name) {
+ $pointer[$name] = $opts;
+ $pointer = &$pointer[$name];
+ }
+
+ $pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options;
+
+ $this->_matching->contain($containments);
+
+ return $this;
+ }
+
+ /**
+ * Returns the current tree of associations to be matched.
+ *
+ * @return array The resulting containments array
+ */
+ public function getMatching(): array
+ {
+ if ($this->_matching === null) {
+ $this->_matching = new static();
+ }
+
+ return $this->_matching->getContain();
+ }
+
+ /**
+ * Returns the fully normalized array of associations that should be eagerly
+ * loaded for a table. The normalized array will restructure the original array
+ * by sorting all associations under one key and special options under another.
+ *
+ * Each of the levels of the associations tree will converted to a Cake\ORM\EagerLoadable
+ * object, that contains all the information required for the association objects
+ * to load the information from the database.
+ *
+ * Additionally it will set an 'instance' key per association containing the
+ * association instance from the corresponding source table
+ *
+ * @param \Cake\ORM\Table $repository The table containing the association that
+ * will be normalized
+ * @return array
+ */
+ public function normalized(Table $repository): array
+ {
+ if ($this->_normalized !== null || empty($this->_containments)) {
+ return (array)$this->_normalized;
+ }
+
+ $contain = [];
+ foreach ($this->_containments as $alias => $options) {
+ if (!empty($options['instance'])) {
+ $contain = $this->_containments;
+ break;
+ }
+ $contain[$alias] = $this->_normalizeContain(
+ $repository,
+ $alias,
+ $options,
+ ['root' => null]
+ );
+ }
+
+ return $this->_normalized = $contain;
+ }
+
+ /**
+ * Formats the containments array so that associations are always set as keys
+ * in the array. This function merges the original associations array with
+ * the new associations provided
+ *
+ * @param array $associations user provided containments array
+ * @param array $original The original containments array to merge
+ * with the new one
+ * @return array
+ */
+ protected function _reformatContain(array $associations, array $original): array
+ {
+ $result = $original;
+
+ foreach ($associations as $table => $options) {
+ $pointer = &$result;
+ if (is_int($table)) {
+ $table = $options;
+ $options = [];
+ }
+
+ if ($options instanceof EagerLoadable) {
+ $options = $options->asContainArray();
+ $table = key($options);
+ $options = current($options);
+ }
+
+ if (isset($this->_containOptions[$table])) {
+ $pointer[$table] = $options;
+ continue;
+ }
+
+ if (strpos($table, '.')) {
+ $path = explode('.', $table);
+ $table = array_pop($path);
+ foreach ($path as $t) {
+ $pointer += [$t => []];
+ $pointer = &$pointer[$t];
+ }
+ }
+
+ if (is_array($options)) {
+ $options = isset($options['config']) ?
+ $options['config'] + $options['associations'] :
+ $options;
+ $options = $this->_reformatContain(
+ $options,
+ $pointer[$table] ?? []
+ );
+ }
+
+ if ($options instanceof Closure) {
+ $options = ['queryBuilder' => $options];
+ }
+
+ $pointer += [$table => []];
+
+ if (isset($options['queryBuilder'], $pointer[$table]['queryBuilder'])) {
+ $first = $pointer[$table]['queryBuilder'];
+ $second = $options['queryBuilder'];
+ $options['queryBuilder'] = function ($query) use ($first, $second) {
+ return $second($first($query));
+ };
+ }
+
+ if (!is_array($options)) {
+ /** @psalm-suppress InvalidArrayOffset */
+ $options = [$options => []];
+ }
+
+ $pointer[$table] = $options + $pointer[$table];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Modifies the passed query to apply joins or any other transformation required
+ * in order to eager load the associations described in the `contain` array.
+ * This method will not modify the query for loading external associations, i.e.
+ * those that cannot be loaded without executing a separate query.
+ *
+ * @param \Cake\ORM\Query $query The query to be modified
+ * @param \Cake\ORM\Table $repository The repository containing the associations
+ * @param bool $includeFields whether to append all fields from the associations
+ * to the passed query. This can be overridden according to the settings defined
+ * per association in the containments array
+ * @return void
+ */
+ public function attachAssociations(Query $query, Table $repository, bool $includeFields): void
+ {
+ if (empty($this->_containments) && $this->_matching === null) {
+ return;
+ }
+
+ $attachable = $this->attachableAssociations($repository);
+ $processed = [];
+ do {
+ foreach ($attachable as $alias => $loadable) {
+ $config = $loadable->getConfig() + [
+ 'aliasPath' => $loadable->aliasPath(),
+ 'propertyPath' => $loadable->propertyPath(),
+ 'includeFields' => $includeFields,
+ ];
+ $loadable->instance()->attachTo($query, $config);
+ $processed[$alias] = true;
+ }
+
+ $newAttachable = $this->attachableAssociations($repository);
+ $attachable = array_diff_key($newAttachable, $processed);
+ } while (!empty($attachable));
+ }
+
+ /**
+ * Returns an array with the associations that can be fetched using a single query,
+ * the array keys are the association aliases and the values will contain an array
+ * with Cake\ORM\EagerLoadable objects.
+ *
+ * @param \Cake\ORM\Table $repository The table containing the associations to be
+ * attached
+ * @return \Cake\ORM\EagerLoadable[]
+ */
+ public function attachableAssociations(Table $repository): array
+ {
+ $contain = $this->normalized($repository);
+ $matching = $this->_matching ? $this->_matching->normalized($repository) : [];
+ $this->_fixStrategies();
+ $this->_loadExternal = [];
+
+ return $this->_resolveJoins($contain, $matching);
+ }
+
+ /**
+ * Returns an array with the associations that need to be fetched using a
+ * separate query, each array value will contain a Cake\ORM\EagerLoadable object.
+ *
+ * @param \Cake\ORM\Table $repository The table containing the associations
+ * to be loaded
+ * @return \Cake\ORM\EagerLoadable[]
+ */
+ public function externalAssociations(Table $repository): array
+ {
+ if ($this->_loadExternal) {
+ return $this->_loadExternal;
+ }
+
+ $this->attachableAssociations($repository);
+
+ return $this->_loadExternal;
+ }
+
+ /**
+ * Auxiliary function responsible for fully normalizing deep associations defined
+ * using `contain()`
+ *
+ * @param \Cake\ORM\Table $parent owning side of the association
+ * @param string $alias name of the association to be loaded
+ * @param array $options list of extra options to use for this association
+ * @param array $paths An array with two values, the first one is a list of dot
+ * separated strings representing associations that lead to this `$alias` in the
+ * chain of associations to be loaded. The second value is the path to follow in
+ * entities' properties to fetch a record of the corresponding association.
+ * @return \Cake\ORM\EagerLoadable Object with normalized associations
+ * @throws \InvalidArgumentException When containments refer to associations that do not exist.
+ */
+ protected function _normalizeContain(Table $parent, string $alias, array $options, array $paths): EagerLoadable
+ {
+ $defaults = $this->_containOptions;
+ $instance = $parent->getAssociation($alias);
+
+ $paths += ['aliasPath' => '', 'propertyPath' => '', 'root' => $alias];
+ $paths['aliasPath'] .= '.' . $alias;
+
+ if (
+ isset($options['matching']) &&
+ $options['matching'] === true
+ ) {
+ $paths['propertyPath'] = '_matchingData.' . $alias;
+ } else {
+ $paths['propertyPath'] .= '.' . $instance->getProperty();
+ }
+
+ $table = $instance->getTarget();
+
+ $extra = array_diff_key($options, $defaults);
+ $config = [
+ 'associations' => [],
+ 'instance' => $instance,
+ 'config' => array_diff_key($options, $extra),
+ 'aliasPath' => trim($paths['aliasPath'], '.'),
+ 'propertyPath' => trim($paths['propertyPath'], '.'),
+ 'targetProperty' => $instance->getProperty(),
+ ];
+ $config['canBeJoined'] = $instance->canBeJoined($config['config']);
+ $eagerLoadable = new EagerLoadable($alias, $config);
+
+ if ($config['canBeJoined']) {
+ $this->_aliasList[$paths['root']][$alias][] = $eagerLoadable;
+ } else {
+ $paths['root'] = $config['aliasPath'];
+ }
+
+ foreach ($extra as $t => $assoc) {
+ $eagerLoadable->addAssociation(
+ $t,
+ $this->_normalizeContain($table, $t, $assoc, $paths)
+ );
+ }
+
+ return $eagerLoadable;
+ }
+
+ /**
+ * Iterates over the joinable aliases list and corrects the fetching strategies
+ * in order to avoid aliases collision in the generated queries.
+ *
+ * This function operates on the array references that were generated by the
+ * _normalizeContain() function.
+ *
+ * @return void
+ */
+ protected function _fixStrategies(): void
+ {
+ foreach ($this->_aliasList as $aliases) {
+ foreach ($aliases as $configs) {
+ if (count($configs) < 2) {
+ continue;
+ }
+ /** @var \Cake\ORM\EagerLoadable $loadable */
+ foreach ($configs as $loadable) {
+ if (strpos($loadable->aliasPath(), '.')) {
+ $this->_correctStrategy($loadable);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Changes the association fetching strategy if required because of duplicate
+ * under the same direct associations chain
+ *
+ * @param \Cake\ORM\EagerLoadable $loadable The association config
+ * @return void
+ */
+ protected function _correctStrategy(EagerLoadable $loadable): void
+ {
+ $config = $loadable->getConfig();
+ $currentStrategy = $config['strategy'] ??
+ 'join';
+
+ if (!$loadable->canBeJoined() || $currentStrategy !== 'join') {
+ return;
+ }
+
+ $config['strategy'] = Association::STRATEGY_SELECT;
+ $loadable->setConfig($config);
+ $loadable->setCanBeJoined(false);
+ }
+
+ /**
+ * Helper function used to compile a list of all associations that can be
+ * joined in the query.
+ *
+ * @param \Cake\ORM\EagerLoadable[] $associations list of associations from which to obtain joins.
+ * @param \Cake\ORM\EagerLoadable[] $matching list of associations that should be forcibly joined.
+ * @return \Cake\ORM\EagerLoadable[]
+ */
+ protected function _resolveJoins(array $associations, array $matching = []): array
+ {
+ $result = [];
+ foreach ($matching as $table => $loadable) {
+ $result[$table] = $loadable;
+ $result += $this->_resolveJoins($loadable->associations(), []);
+ }
+ foreach ($associations as $table => $loadable) {
+ $inMatching = isset($matching[$table]);
+ if (!$inMatching && $loadable->canBeJoined()) {
+ $result[$table] = $loadable;
+ $result += $this->_resolveJoins($loadable->associations(), []);
+ continue;
+ }
+
+ if ($inMatching) {
+ $this->_correctStrategy($loadable);
+ }
+
+ $loadable->setCanBeJoined(false);
+ $this->_loadExternal[] = $loadable;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Decorates the passed statement object in order to inject data from associations
+ * that cannot be joined directly.
+ *
+ * @param \Cake\ORM\Query $query The query for which to eager load external
+ * associations
+ * @param \Cake\Database\StatementInterface $statement The statement created after executing the $query
+ * @return \Cake\Database\StatementInterface statement modified statement with extra loaders
+ * @throws \RuntimeException
+ */
+ public function loadExternal(Query $query, StatementInterface $statement): StatementInterface
+ {
+ $table = $query->getRepository();
+ $external = $this->externalAssociations($table);
+ if (empty($external)) {
+ return $statement;
+ }
+
+ $driver = $query->getConnection()->getDriver();
+ [$collected, $statement] = $this->_collectKeys($external, $query, $statement);
+
+ // No records found, skip trying to attach associations.
+ if (empty($collected) && $statement->count() === 0) {
+ return $statement;
+ }
+
+ foreach ($external as $meta) {
+ $contain = $meta->associations();
+ $instance = $meta->instance();
+ $config = $meta->getConfig();
+ $alias = $instance->getSource()->getAlias();
+ $path = $meta->aliasPath();
+
+ $requiresKeys = $instance->requiresKeys($config);
+ if ($requiresKeys) {
+ // If the path or alias has no key the required association load will fail.
+ // Nested paths are not subject to this condition because they could
+ // be attached to joined associations.
+ if (
+ strpos($path, '.') === false &&
+ (!array_key_exists($path, $collected) || !array_key_exists($alias, $collected[$path]))
+ ) {
+ $message = "Unable to load `{$path}` association. Ensure foreign key in `{$alias}` is selected.";
+ throw new InvalidArgumentException($message);
+ }
+
+ // If the association foreign keys are missing skip loading
+ // as the association could be optional.
+ if (empty($collected[$path][$alias])) {
+ continue;
+ }
+ }
+
+ $keys = $collected[$path][$alias] ?? null;
+ $f = $instance->eagerLoader(
+ $config + [
+ 'query' => $query,
+ 'contain' => $contain,
+ 'keys' => $keys,
+ 'nestKey' => $meta->aliasPath(),
+ ]
+ );
+ $statement = new CallbackStatement($statement, $driver, $f);
+ }
+
+ return $statement;
+ }
+
+ /**
+ * Returns an array having as keys a dotted path of associations that participate
+ * in this eager loader. The values of the array will contain the following keys
+ *
+ * - alias: The association alias
+ * - instance: The association instance
+ * - canBeJoined: Whether or not the association will be loaded using a JOIN
+ * - entityClass: The entity that should be used for hydrating the results
+ * - nestKey: A dotted path that can be used to correctly insert the data into the results.
+ * - matching: Whether or not it is an association loaded through `matching()`.
+ *
+ * @param \Cake\ORM\Table $table The table containing the association that
+ * will be normalized
+ * @return array
+ */
+ public function associationsMap(Table $table): array
+ {
+ $map = [];
+
+ if (!$this->getMatching() && !$this->getContain() && empty($this->_joinsMap)) {
+ return $map;
+ }
+
+ /** @psalm-suppress PossiblyNullReference */
+ $map = $this->_buildAssociationsMap($map, $this->_matching->normalized($table), true);
+ $map = $this->_buildAssociationsMap($map, $this->normalized($table));
+ $map = $this->_buildAssociationsMap($map, $this->_joinsMap);
+
+ return $map;
+ }
+
+ /**
+ * An internal method to build a map which is used for the return value of the
+ * associationsMap() method.
+ *
+ * @param array $map An initial array for the map.
+ * @param \Cake\ORM\EagerLoadable[] $level An array of EagerLoadable instances.
+ * @param bool $matching Whether or not it is an association loaded through `matching()`.
+ * @return array
+ */
+ protected function _buildAssociationsMap(array $map, array $level, bool $matching = false): array
+ {
+ foreach ($level as $assoc => $meta) {
+ $canBeJoined = $meta->canBeJoined();
+ $instance = $meta->instance();
+ $associations = $meta->associations();
+ $forMatching = $meta->forMatching();
+ $map[] = [
+ 'alias' => $assoc,
+ 'instance' => $instance,
+ 'canBeJoined' => $canBeJoined,
+ 'entityClass' => $instance->getTarget()->getEntityClass(),
+ 'nestKey' => $canBeJoined ? $assoc : $meta->aliasPath(),
+ 'matching' => $forMatching ?? $matching,
+ 'targetProperty' => $meta->targetProperty(),
+ ];
+ if ($canBeJoined && $associations) {
+ $map = $this->_buildAssociationsMap($map, $associations, $matching);
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * Registers a table alias, typically loaded as a join in a query, as belonging to
+ * an association. This helps hydrators know what to do with the columns coming
+ * from such joined table.
+ *
+ * @param string $alias The table alias as it appears in the query.
+ * @param \Cake\ORM\Association $assoc The association object the alias represents;
+ * will be normalized
+ * @param bool $asMatching Whether or not this join results should be treated as a
+ * 'matching' association.
+ * @param string $targetProperty The property name where the results of the join should be nested at.
+ * If not passed, the default property for the association will be used.
+ * @return void
+ */
+ public function addToJoinsMap(
+ string $alias,
+ Association $assoc,
+ bool $asMatching = false,
+ ?string $targetProperty = null
+ ): void {
+ $this->_joinsMap[$alias] = new EagerLoadable($alias, [
+ 'aliasPath' => $alias,
+ 'instance' => $assoc,
+ 'canBeJoined' => true,
+ 'forMatching' => $asMatching,
+ 'targetProperty' => $targetProperty ?: $assoc->getProperty(),
+ ]);
+ }
+
+ /**
+ * Helper function used to return the keys from the query records that will be used
+ * to eagerly load associations.
+ *
+ * @param \Cake\ORM\EagerLoadable[] $external the list of external associations to be loaded
+ * @param \Cake\ORM\Query $query The query from which the results where generated
+ * @param \Cake\Database\StatementInterface $statement The statement to work on
+ * @return array
+ */
+ protected function _collectKeys(array $external, Query $query, $statement): array
+ {
+ $collectKeys = [];
+ foreach ($external as $meta) {
+ $instance = $meta->instance();
+ if (!$instance->requiresKeys($meta->getConfig())) {
+ continue;
+ }
+
+ $source = $instance->getSource();
+ $keys = $instance->type() === Association::MANY_TO_ONE ?
+ (array)$instance->getForeignKey() :
+ (array)$instance->getBindingKey();
+
+ $alias = $source->getAlias();
+ $pkFields = [];
+ foreach ($keys as $key) {
+ $pkFields[] = key($query->aliasField($key, $alias));
+ }
+ $collectKeys[$meta->aliasPath()] = [$alias, $pkFields, count($pkFields) === 1];
+ }
+ if (empty($collectKeys)) {
+ return [[], $statement];
+ }
+
+ if (!($statement instanceof BufferedStatement)) {
+ $statement = new BufferedStatement($statement, $query->getConnection()->getDriver());
+ }
+
+ return [$this->_groupKeys($statement, $collectKeys), $statement];
+ }
+
+ /**
+ * Helper function used to iterate a statement and extract the columns
+ * defined in $collectKeys
+ *
+ * @param \Cake\Database\Statement\BufferedStatement $statement The statement to read from.
+ * @param array $collectKeys The keys to collect
+ * @return array
+ */
+ protected function _groupKeys(BufferedStatement $statement, array $collectKeys): array
+ {
+ $keys = [];
+ foreach (($statement->fetchAll('assoc') ?: []) as $result) {
+ foreach ($collectKeys as $nestKey => $parts) {
+ if ($parts[2] === true) {
+ // Missed joins will have null in the results.
+ if (!array_key_exists($parts[1][0], $result)) {
+ continue;
+ }
+ // Assign empty array to avoid not found association when optional.
+ if (!isset($result[$parts[1][0]])) {
+ if (!isset($keys[$nestKey][$parts[0]])) {
+ $keys[$nestKey][$parts[0]] = [];
+ }
+ } else {
+ $value = $result[$parts[1][0]];
+ $keys[$nestKey][$parts[0]][$value] = $value;
+ }
+ continue;
+ }
+
+ // Handle composite keys.
+ $collected = [];
+ foreach ($parts[1] as $key) {
+ $collected[] = $result[$key];
+ }
+ $keys[$nestKey][$parts[0]][implode(';', $collected)] = $collected;
+ }
+ }
+ $statement->rewind();
+
+ return $keys;
+ }
+
+ /**
+ * Handles cloning eager loaders and eager loadables.
+ *
+ * @return void
+ */
+ public function __clone()
+ {
+ if ($this->_matching) {
+ $this->_matching = clone $this->_matching;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Entity.php b/app/vendor/cakephp/cakephp/src/ORM/Entity.php
new file mode 100644
index 000000000..a71678754
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Entity.php
@@ -0,0 +1,85 @@
+ 1, 'name' => 'Andrew'])
+ * ```
+ *
+ * @param array $properties hash of properties to set in this entity
+ * @param array $options list of options to use when creating this entity
+ */
+ public function __construct(array $properties = [], array $options = [])
+ {
+ $options += [
+ 'useSetters' => true,
+ 'markClean' => false,
+ 'markNew' => null,
+ 'guard' => false,
+ 'source' => null,
+ ];
+
+ if (!empty($options['source'])) {
+ $this->setSource($options['source']);
+ }
+
+ if ($options['markNew'] !== null) {
+ $this->setNew($options['markNew']);
+ }
+
+ if (!empty($properties) && $options['markClean'] && !$options['useSetters']) {
+ $this->_fields = $properties;
+
+ return;
+ }
+
+ if (!empty($properties)) {
+ $this->set($properties, [
+ 'setter' => $options['useSetters'],
+ 'guard' => $options['guard'],
+ ]);
+ }
+
+ if ($options['markClean']) {
+ $this->clean();
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Exception/MissingBehaviorException.php b/app/vendor/cakephp/cakephp/src/ORM/Exception/MissingBehaviorException.php
new file mode 100644
index 000000000..fe6411eb0
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Exception/MissingBehaviorException.php
@@ -0,0 +1,28 @@
+_entity = $entity;
+ if (is_array($message)) {
+ $errors = [];
+ foreach (Hash::flatten($entity->getErrors()) as $field => $error) {
+ $errors[] = $field . ': "' . $error . '"';
+ }
+ if ($errors) {
+ $message[] = implode(', ', $errors);
+ $this->_messageTemplate = 'Entity %s failure. Found the following errors (%s).';
+ }
+ }
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * Get the passed in entity
+ *
+ * @return \Cake\Datasource\EntityInterface
+ */
+ public function getEntity(): EntityInterface
+ {
+ return $this->_entity;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Exception/RolledbackTransactionException.php b/app/vendor/cakephp/cakephp/src/ORM/Exception/RolledbackTransactionException.php
new file mode 100644
index 000000000..b1f107bd8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Exception/RolledbackTransactionException.php
@@ -0,0 +1,29 @@
+_getQuery($entities, $contain, $source);
+ $associations = array_keys($query->getContain());
+
+ $entities = $this->_injectResults($entities, $query, $associations, $source);
+
+ return $returnSingle ? array_shift($entities) : $entities;
+ }
+
+ /**
+ * Builds a query for loading the passed list of entity objects along with the
+ * associations specified in $contain.
+ *
+ * @param \Cake\Collection\CollectionInterface $objects The original entities
+ * @param array $contain The associations to be loaded
+ * @param \Cake\ORM\Table $source The table to use for fetching the top level entities
+ * @return \Cake\ORM\Query
+ */
+ protected function _getQuery(CollectionInterface $objects, array $contain, Table $source): Query
+ {
+ $primaryKey = $source->getPrimaryKey();
+ $method = is_string($primaryKey) ? 'get' : 'extract';
+
+ $keys = $objects->map(function ($entity) use ($primaryKey, $method) {
+ return $entity->{$method}($primaryKey);
+ });
+
+ $query = $source
+ ->find()
+ ->select((array)$primaryKey)
+ ->where(function ($exp, $q) use ($primaryKey, $keys, $source) {
+ /**
+ * @var \Cake\Database\Expression\QueryExpression $exp
+ * @var \Cake\ORM\Query $q
+ */
+ if (is_array($primaryKey) && count($primaryKey) === 1) {
+ $primaryKey = current($primaryKey);
+ }
+
+ if (is_string($primaryKey)) {
+ return $exp->in($source->aliasField($primaryKey), $keys->toList());
+ }
+
+ $types = array_intersect_key($q->getDefaultTypes(), array_flip($primaryKey));
+ $primaryKey = array_map([$source, 'aliasField'], $primaryKey);
+
+ return new TupleComparison($primaryKey, $keys->toList(), $types, 'IN');
+ })
+ ->enableAutoFields()
+ ->contain($contain);
+
+ foreach ($query->getEagerLoader()->attachableAssociations($source) as $loadable) {
+ $config = $loadable->getConfig();
+ $config['includeFields'] = true;
+ $loadable->setConfig($config);
+ }
+
+ return $query;
+ }
+
+ /**
+ * Returns a map of property names where the association results should be injected
+ * in the top level entities.
+ *
+ * @param \Cake\ORM\Table $source The table having the top level associations
+ * @param string[] $associations The name of the top level associations
+ * @return string[]
+ */
+ protected function _getPropertyMap(Table $source, array $associations): array
+ {
+ $map = [];
+ $container = $source->associations();
+ foreach ($associations as $assoc) {
+ /** @psalm-suppress PossiblyNullReference */
+ $map[$assoc] = $container->get($assoc)->getProperty();
+ }
+
+ return $map;
+ }
+
+ /**
+ * Injects the results of the eager loader query into the original list of
+ * entities.
+ *
+ * @param \Cake\Datasource\EntityInterface[]|\Traversable $objects The original list of entities
+ * @param \Cake\Collection\CollectionInterface|\Cake\ORM\Query $results The loaded results
+ * @param string[] $associations The top level associations that were loaded
+ * @param \Cake\ORM\Table $source The table where the entities came from
+ * @return array
+ */
+ protected function _injectResults(iterable $objects, $results, array $associations, Table $source): array
+ {
+ $injected = [];
+ $properties = $this->_getPropertyMap($source, $associations);
+ $primaryKey = (array)$source->getPrimaryKey();
+ $results = $results
+ ->indexBy(function ($e) use ($primaryKey) {
+ /** @var \Cake\Datasource\EntityInterface $e */
+ return implode(';', $e->extract($primaryKey));
+ })
+ ->toArray();
+
+ foreach ($objects as $k => $object) {
+ $key = implode(';', $object->extract($primaryKey));
+ if (!isset($results[$key])) {
+ $injected[$k] = $object;
+ continue;
+ }
+
+ /** @var \Cake\Datasource\EntityInterface $loaded */
+ $loaded = $results[$key];
+ foreach ($associations as $assoc) {
+ $property = $properties[$assoc];
+ $object->set($property, $loaded->get($property), ['useSetters' => false]);
+ $object->setDirty($property, false);
+ }
+ $injected[$k] = $object;
+ }
+
+ return $injected;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorAwareTrait.php b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorAwareTrait.php
new file mode 100644
index 000000000..3c17d4376
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorAwareTrait.php
@@ -0,0 +1,61 @@
+_tableLocator = $tableLocator;
+
+ return $this;
+ }
+
+ /**
+ * Gets the table locator.
+ *
+ * @return \Cake\ORM\Locator\LocatorInterface
+ */
+ public function getTableLocator(): LocatorInterface
+ {
+ if ($this->_tableLocator === null) {
+ /** @psalm-suppress InvalidPropertyAssignmentValue */
+ $this->_tableLocator = FactoryLocator::get('Table');
+ }
+
+ /** @var \Cake\ORM\Locator\LocatorInterface */
+ return $this->_tableLocator;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorInterface.php b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorInterface.php
new file mode 100644
index 000000000..68db76d87
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Locator/LocatorInterface.php
@@ -0,0 +1,67 @@
+
+ */
+ protected $instances = [];
+
+ /**
+ * Contains a list of Table objects that were created out of the
+ * built-in Table class. The list is indexed by table alias
+ *
+ * @var \Cake\ORM\Table[]
+ */
+ protected $_fallbacked = [];
+
+ /**
+ * Fallback class to use
+ *
+ * @var string
+ * @psalm-var class-string<\Cake\ORM\Table>
+ */
+ protected $fallbackClassName = Table::class;
+
+ /**
+ * Whether fallback class should be used if a table class could not be found.
+ *
+ * @var bool
+ */
+ protected $allowFallbackClass = true;
+
+ /**
+ * Constructor.
+ *
+ * @param array|null $locations Locations where tables should be looked for.
+ * If none provided, the default `Model\Table` under your app's namespace is used.
+ */
+ public function __construct(?array $locations = null)
+ {
+ if ($locations === null) {
+ $locations = [
+ 'Model/Table',
+ ];
+ }
+
+ foreach ($locations as $location) {
+ $this->addLocation($location);
+ }
+ }
+
+ /**
+ * Set if fallback class should be used.
+ *
+ * Controls whether a fallback class should be used to create a table
+ * instance if a concrete class for alias used in `get()` could not be found.
+ *
+ * @param bool $allow Flag to enable or disable fallback
+ * @return $this
+ */
+ public function allowFallbackClass(bool $allow)
+ {
+ $this->allowFallbackClass = $allow;
+
+ return $this;
+ }
+
+ /**
+ * Set fallback class name.
+ *
+ * The class that should be used to create a table instance if a concrete
+ * class for alias used in `get()` could not be found. Defaults to
+ * `Cake\ORM\Table`.
+ *
+ * @param string $className Fallback class name
+ * @return $this
+ * @psalm-param class-string<\Cake\ORM\Table> $className
+ */
+ public function setFallbackClassName($className)
+ {
+ $this->fallbackClassName = $className;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setConfig($alias, $options = null)
+ {
+ if (!is_string($alias)) {
+ $this->_config = $alias;
+
+ return $this;
+ }
+
+ if (isset($this->instances[$alias])) {
+ throw new RuntimeException(sprintf(
+ 'You cannot configure "%s", it has already been constructed.',
+ $alias
+ ));
+ }
+
+ $this->_config[$alias] = $options;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getConfig(?string $alias = null): array
+ {
+ if ($alias === null) {
+ return $this->_config;
+ }
+
+ return $this->_config[$alias] ?? [];
+ }
+
+ /**
+ * Get a table instance from the registry.
+ *
+ * Tables are only created once until the registry is flushed.
+ * This means that aliases must be unique across your application.
+ * This is important because table associations are resolved at runtime
+ * and cyclic references need to be handled correctly.
+ *
+ * The options that can be passed are the same as in Cake\ORM\Table::__construct(), but the
+ * `className` key is also recognized.
+ *
+ * ### Options
+ *
+ * - `className` Define the specific class name to use. If undefined, CakePHP will generate the
+ * class name based on the alias. For example 'Users' would result in
+ * `App\Model\Table\UsersTable` being used. If this class does not exist,
+ * then the default `Cake\ORM\Table` class will be used. By setting the `className`
+ * option you can define the specific class to use. The className option supports
+ * plugin short class references {@link \Cake\Core\App::shortName()}.
+ * - `table` Define the table name to use. If undefined, this option will default to the underscored
+ * version of the alias name.
+ * - `connection` Inject the specific connection object to use. If this option and `connectionName` are undefined,
+ * The table class' `defaultConnectionName()` method will be invoked to fetch the connection name.
+ * - `connectionName` Define the connection name to use. The named connection will be fetched from
+ * {@link \Cake\Datasource\ConnectionManager}.
+ *
+ * *Note* If your `$alias` uses plugin syntax only the name part will be used as
+ * key in the registry. This means that if two plugins, or a plugin and app provide
+ * the same alias, the registry will only store the first instance.
+ *
+ * @param string $alias The alias name you want to get. Should be in CamelCase format.
+ * @param array $options The options you want to build the table with.
+ * If a table has already been loaded the options will be ignored.
+ * @return \Cake\ORM\Table
+ * @throws \RuntimeException When you try to configure an alias that already exists.
+ */
+ public function get(string $alias, array $options = []): Table
+ {
+ /** @var \Cake\ORM\Table */
+ return parent::get($alias, $options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function createInstance(string $alias, array $options)
+ {
+ if (strpos($alias, '\\') === false) {
+ [, $classAlias] = pluginSplit($alias);
+ $options = ['alias' => $classAlias] + $options;
+ } elseif (!isset($options['alias'])) {
+ $options['className'] = $alias;
+ /** @psalm-suppress PossiblyFalseOperand */
+ $alias = substr($alias, strrpos($alias, '\\') + 1, -5);
+ }
+
+ if (isset($this->_config[$alias])) {
+ $options += $this->_config[$alias];
+ }
+
+ $allowFallbackClass = $options['allowFallbackClass'] ?? $this->allowFallbackClass;
+ $className = $this->_getClassName($alias, $options);
+ if ($className) {
+ $options['className'] = $className;
+ } elseif ($allowFallbackClass) {
+ if (empty($options['className'])) {
+ $options['className'] = $alias;
+ }
+ if (!isset($options['table']) && strpos($options['className'], '\\') === false) {
+ [, $table] = pluginSplit($options['className']);
+ $options['table'] = Inflector::underscore($table);
+ }
+ $options['className'] = $this->fallbackClassName;
+ } else {
+ $message = $options['className'] ?? $alias;
+ $message = '`' . $message . '`';
+ if (strpos($message, '\\') === false) {
+ $message = 'for alias ' . $message;
+ }
+ throw new MissingTableClassException([$message]);
+ }
+
+ if (empty($options['connection'])) {
+ if (!empty($options['connectionName'])) {
+ $connectionName = $options['connectionName'];
+ } else {
+ /** @var \Cake\ORM\Table $className */
+ $className = $options['className'];
+ $connectionName = $className::defaultConnectionName();
+ }
+ $options['connection'] = ConnectionManager::get($connectionName);
+ }
+ if (empty($options['associations'])) {
+ $associations = new AssociationCollection($this);
+ $options['associations'] = $associations;
+ }
+
+ $options['registryAlias'] = $alias;
+ $instance = $this->_create($options);
+
+ if ($options['className'] === $this->fallbackClassName) {
+ $this->_fallbacked[$alias] = $instance;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Gets the table class name.
+ *
+ * @param string $alias The alias name you want to get. Should be in CamelCase format.
+ * @param array $options Table options array.
+ * @return string|null
+ */
+ protected function _getClassName(string $alias, array $options = []): ?string
+ {
+ if (empty($options['className'])) {
+ $options['className'] = $alias;
+ }
+
+ if (strpos($options['className'], '\\') !== false && class_exists($options['className'])) {
+ return $options['className'];
+ }
+
+ foreach ($this->locations as $location) {
+ $class = App::className($options['className'], $location, 'Table');
+ if ($class !== null) {
+ return $class;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Wrapper for creating table instances
+ *
+ * @param array $options The alias to check for.
+ * @return \Cake\ORM\Table
+ */
+ protected function _create(array $options): Table
+ {
+ /** @var \Cake\ORM\Table */
+ return new $options['className']($options);
+ }
+
+ /**
+ * Set a Table instance.
+ *
+ * @param string $alias The alias to set.
+ * @param \Cake\ORM\Table $repository The Table to set.
+ * @return \Cake\ORM\Table
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ public function set(string $alias, RepositoryInterface $repository): Table
+ {
+ return $this->instances[$alias] = $repository;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clear(): void
+ {
+ parent::clear();
+
+ $this->_fallbacked = [];
+ $this->_config = [];
+ }
+
+ /**
+ * Returns the list of tables that were created by this registry that could
+ * not be instantiated from a specific subclass. This method is useful for
+ * debugging common mistakes when setting up associations or created new table
+ * classes.
+ *
+ * @return \Cake\ORM\Table[]
+ */
+ public function genericInstances(): array
+ {
+ return $this->_fallbacked;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function remove(string $alias): void
+ {
+ parent::remove($alias);
+
+ unset($this->_fallbacked[$alias]);
+ }
+
+ /**
+ * Adds a location where tables should be looked for.
+ *
+ * @param string $location Location to add.
+ * @return $this
+ * @since 3.8.0
+ */
+ public function addLocation(string $location)
+ {
+ $location = str_replace('\\', '/', $location);
+ $this->locations[] = trim($location, '/');
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Marshaller.php b/app/vendor/cakephp/cakephp/src/ORM/Marshaller.php
new file mode 100644
index 000000000..f79d5569a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Marshaller.php
@@ -0,0 +1,885 @@
+_table = $table;
+ }
+
+ /**
+ * Build the map of property => marshalling callable.
+ *
+ * @param array $data The data being marshalled.
+ * @param array $options List of options containing the 'associated' key.
+ * @throws \InvalidArgumentException When associations do not exist.
+ * @return array
+ */
+ protected function _buildPropertyMap(array $data, array $options): array
+ {
+ $map = [];
+ $schema = $this->_table->getSchema();
+
+ // Is a concrete column?
+ foreach (array_keys($data) as $prop) {
+ $prop = (string)$prop;
+ $columnType = $schema->getColumnType($prop);
+ if ($columnType) {
+ $map[$prop] = function ($value, $entity) use ($columnType) {
+ return TypeFactory::build($columnType)->marshal($value);
+ };
+ }
+ }
+
+ // Map associations
+ if (!isset($options['associated'])) {
+ $options['associated'] = [];
+ }
+ $include = $this->_normalizeAssociations($options['associated']);
+ foreach ($include as $key => $nested) {
+ if (is_int($key) && is_scalar($nested)) {
+ $key = $nested;
+ $nested = [];
+ }
+ // If the key is not a special field like _ids or _joinData
+ // it is a missing association that we should error on.
+ if (!$this->_table->hasAssociation($key)) {
+ if (substr($key, 0, 1) !== '_') {
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot marshal data for "%s" association. It is not associated with "%s".',
+ (string)$key,
+ $this->_table->getAlias()
+ ));
+ }
+ continue;
+ }
+ $assoc = $this->_table->getAssociation($key);
+
+ if (isset($options['forceNew'])) {
+ $nested['forceNew'] = $options['forceNew'];
+ }
+ if (isset($options['isMerge'])) {
+ $callback = function ($value, $entity) use ($assoc, $nested) {
+ /** @var \Cake\Datasource\EntityInterface $entity */
+ $options = $nested + ['associated' => [], 'association' => $assoc];
+
+ return $this->_mergeAssociation($entity->get($assoc->getProperty()), $assoc, $value, $options);
+ };
+ } else {
+ $callback = function ($value, $entity) use ($assoc, $nested) {
+ $options = $nested + ['associated' => []];
+
+ return $this->_marshalAssociation($assoc, $value, $options);
+ };
+ }
+ $map[$assoc->getProperty()] = $callback;
+ }
+
+ $behaviors = $this->_table->behaviors();
+ foreach ($behaviors->loaded() as $name) {
+ $behavior = $behaviors->get($name);
+ if ($behavior instanceof PropertyMarshalInterface) {
+ $map += $behavior->buildMarshalMap($this, $map, $options);
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * Hydrate one entity and its associated data.
+ *
+ * ### Options:
+ *
+ * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied.
+ * Defaults to true/default.
+ * - associated: Associations listed here will be marshalled as well. Defaults to null.
+ * - fields: An allowed list of fields to be assigned to the entity. If not present,
+ * the accessible fields list in the entity will be used. Defaults to null.
+ * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null
+ * - forceNew: When enabled, belongsToMany associations will have 'new' entities created
+ * when primary key values are set, and a record does not already exist. Normally primary key
+ * on missing entities would be ignored. Defaults to false.
+ *
+ * The above options can be used in each nested `associated` array. In addition to the above
+ * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations.
+ * When true this option restricts the request data to only be read from `_ids`.
+ *
+ * ```
+ * $result = $marshaller->one($data, [
+ * 'associated' => ['Tags' => ['onlyIds' => true]]
+ * ]);
+ * ```
+ *
+ * ```
+ * $result = $marshaller->one($data, [
+ * 'associated' => [
+ * 'Tags' => ['accessibleFields' => ['*' => true]]
+ * ]
+ * ]);
+ * ```
+ *
+ * @param array $data The data to hydrate.
+ * @param array $options List of options
+ * @return \Cake\Datasource\EntityInterface
+ * @see \Cake\ORM\Table::newEntity()
+ * @see \Cake\ORM\Entity::$_accessible
+ */
+ public function one(array $data, array $options = []): EntityInterface
+ {
+ [$data, $options] = $this->_prepareDataAndOptions($data, $options);
+
+ $primaryKey = (array)$this->_table->getPrimaryKey();
+ $entityClass = $this->_table->getEntityClass();
+ $entity = new $entityClass();
+ $entity->setSource($this->_table->getRegistryAlias());
+
+ if (isset($options['accessibleFields'])) {
+ foreach ((array)$options['accessibleFields'] as $key => $value) {
+ $entity->setAccess($key, $value);
+ }
+ }
+ $errors = $this->_validate($data, $options, true);
+
+ $options['isMerge'] = false;
+ $propertyMap = $this->_buildPropertyMap($data, $options);
+ $properties = [];
+ foreach ($data as $key => $value) {
+ if (!empty($errors[$key])) {
+ if ($entity instanceof InvalidPropertyInterface) {
+ $entity->setInvalidField($key, $value);
+ }
+ continue;
+ }
+
+ if ($value === '' && in_array($key, $primaryKey, true)) {
+ // Skip marshalling '' for pk fields.
+ continue;
+ }
+ if (isset($propertyMap[$key])) {
+ $properties[$key] = $propertyMap[$key]($value, $entity);
+ } else {
+ $properties[$key] = $value;
+ }
+ }
+
+ if (isset($options['fields'])) {
+ foreach ((array)$options['fields'] as $field) {
+ if (array_key_exists($field, $properties)) {
+ $entity->set($field, $properties[$field]);
+ }
+ }
+ } else {
+ $entity->set($properties);
+ }
+
+ // Don't flag clean association entities as
+ // dirty so we don't persist empty records.
+ foreach ($properties as $field => $value) {
+ if ($value instanceof EntityInterface) {
+ $entity->setDirty($field, $value->isDirty());
+ }
+ }
+
+ $entity->setErrors($errors);
+ $this->dispatchAfterMarshal($entity, $data, $options);
+
+ return $entity;
+ }
+
+ /**
+ * Returns the validation errors for a data set based on the passed options
+ *
+ * @param array $data The data to validate.
+ * @param array $options The options passed to this marshaller.
+ * @param bool $isNew Whether it is a new entity or one to be updated.
+ * @return array The list of validation errors.
+ * @throws \RuntimeException If no validator can be created.
+ */
+ protected function _validate(array $data, array $options, bool $isNew): array
+ {
+ if (!$options['validate']) {
+ return [];
+ }
+
+ $validator = null;
+ if ($options['validate'] === true) {
+ $validator = $this->_table->getValidator();
+ } elseif (is_string($options['validate'])) {
+ $validator = $this->_table->getValidator($options['validate']);
+ } elseif (is_object($options['validate'])) {
+ /** @var \Cake\Validation\Validator $validator */
+ $validator = $options['validate'];
+ }
+
+ if ($validator === null) {
+ throw new RuntimeException(
+ sprintf('validate must be a boolean, a string or an object. Got %s.', getTypeName($options['validate']))
+ );
+ }
+
+ return $validator->validate($data, $isNew);
+ }
+
+ /**
+ * Returns data and options prepared to validate and marshall.
+ *
+ * @param array $data The data to prepare.
+ * @param array $options The options passed to this marshaller.
+ * @return array An array containing prepared data and options.
+ */
+ protected function _prepareDataAndOptions(array $data, array $options): array
+ {
+ $options += ['validate' => true];
+
+ $tableName = $this->_table->getAlias();
+ if (isset($data[$tableName])) {
+ $data += $data[$tableName];
+ unset($data[$tableName]);
+ }
+
+ $data = new ArrayObject($data);
+ $options = new ArrayObject($options);
+ $this->_table->dispatchEvent('Model.beforeMarshal', compact('data', 'options'));
+
+ return [(array)$data, (array)$options];
+ }
+
+ /**
+ * Create a new sub-marshaller and marshal the associated data.
+ *
+ * @param \Cake\ORM\Association $assoc The association to marshall
+ * @param mixed $value The data to hydrate. If not an array, this method will return null.
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
+ */
+ protected function _marshalAssociation(Association $assoc, $value, array $options)
+ {
+ if (!is_array($value)) {
+ return null;
+ }
+ $targetTable = $assoc->getTarget();
+ $marshaller = $targetTable->marshaller();
+ $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
+ $type = $assoc->type();
+ if (in_array($type, $types, true)) {
+ return $marshaller->one($value, $options);
+ }
+ if ($type === Association::ONE_TO_MANY || $type === Association::MANY_TO_MANY) {
+ $hasIds = array_key_exists('_ids', $value);
+ $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
+
+ if ($hasIds && is_array($value['_ids'])) {
+ return $this->_loadAssociatedByIds($assoc, $value['_ids']);
+ }
+ if ($hasIds || $onlyIds) {
+ return [];
+ }
+ }
+ if ($type === Association::MANY_TO_MANY) {
+ /** @psalm-suppress ArgumentTypeCoercion */
+ return $marshaller->_belongsToMany($assoc, $value, $options);
+ }
+
+ return $marshaller->many($value, $options);
+ }
+
+ /**
+ * Hydrate many entities and their associated data.
+ *
+ * ### Options:
+ *
+ * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied.
+ * Defaults to true/default.
+ * - associated: Associations listed here will be marshalled as well. Defaults to null.
+ * - fields: An allowed list of fields to be assigned to the entity. If not present,
+ * the accessible fields list in the entity will be used. Defaults to null.
+ * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null
+ * - forceNew: When enabled, belongsToMany associations will have 'new' entities created
+ * when primary key values are set, and a record does not already exist. Normally primary key
+ * on missing entities would be ignored. Defaults to false.
+ *
+ * @param array $data The data to hydrate.
+ * @param array $options List of options
+ * @return \Cake\Datasource\EntityInterface[] An array of hydrated records.
+ * @see \Cake\ORM\Table::newEntities()
+ * @see \Cake\ORM\Entity::$_accessible
+ */
+ public function many(array $data, array $options = []): array
+ {
+ $output = [];
+ foreach ($data as $record) {
+ if (!is_array($record)) {
+ continue;
+ }
+ $output[] = $this->one($record, $options);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Marshals data for belongsToMany associations.
+ *
+ * Builds the related entities and handles the special casing
+ * for junction table entities.
+ *
+ * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshal.
+ * @param array $data The data to convert into entities.
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface[] An array of built entities.
+ * @throws \BadMethodCallException
+ * @throws \InvalidArgumentException
+ * @throws \RuntimeException
+ */
+ protected function _belongsToMany(BelongsToMany $assoc, array $data, array $options = []): array
+ {
+ $associated = $options['associated'] ?? [];
+ $forceNew = $options['forceNew'] ?? false;
+
+ $data = array_values($data);
+
+ $target = $assoc->getTarget();
+ $primaryKey = array_flip((array)$target->getPrimaryKey());
+ $records = $conditions = [];
+ $primaryCount = count($primaryKey);
+
+ foreach ($data as $i => $row) {
+ if (!is_array($row)) {
+ continue;
+ }
+ if (array_intersect_key($primaryKey, $row) === $primaryKey) {
+ $keys = array_intersect_key($row, $primaryKey);
+ if (count($keys) === $primaryCount) {
+ $rowConditions = [];
+ foreach ($keys as $key => $value) {
+ $rowConditions[][$target->aliasField($key)] = $value;
+ }
+
+ if ($forceNew && !$target->exists($rowConditions)) {
+ $records[$i] = $this->one($row, $options);
+ }
+
+ $conditions = array_merge($conditions, $rowConditions);
+ }
+ } else {
+ $records[$i] = $this->one($row, $options);
+ }
+ }
+
+ if (!empty($conditions)) {
+ $query = $target->find();
+ $query->andWhere(function ($exp) use ($conditions) {
+ /** @var \Cake\Database\Expression\QueryExpression $exp */
+ return $exp->or($conditions);
+ });
+
+ $keyFields = array_keys($primaryKey);
+
+ $existing = [];
+ foreach ($query as $row) {
+ $k = implode(';', $row->extract($keyFields));
+ $existing[$k] = $row;
+ }
+
+ foreach ($data as $i => $row) {
+ $key = [];
+ foreach ($keyFields as $k) {
+ if (isset($row[$k])) {
+ $key[] = $row[$k];
+ }
+ }
+ $key = implode(';', $key);
+
+ // Update existing record and child associations
+ if (isset($existing[$key])) {
+ $records[$i] = $this->merge($existing[$key], $data[$i], $options);
+ }
+ }
+ }
+
+ $jointMarshaller = $assoc->junction()->marshaller();
+
+ $nested = [];
+ if (isset($associated['_joinData'])) {
+ $nested = (array)$associated['_joinData'];
+ }
+
+ foreach ($records as $i => $record) {
+ // Update junction table data in _joinData.
+ if (isset($data[$i]['_joinData'])) {
+ $joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested);
+ $record->set('_joinData', $joinData);
+ }
+ }
+
+ return $records;
+ }
+
+ /**
+ * Loads a list of belongs to many from ids.
+ *
+ * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association.
+ * @param array $ids The list of ids to load.
+ * @return \Cake\Datasource\EntityInterface[] An array of entities.
+ */
+ protected function _loadAssociatedByIds(Association $assoc, array $ids): array
+ {
+ if (empty($ids)) {
+ return [];
+ }
+
+ $target = $assoc->getTarget();
+ $primaryKey = (array)$target->getPrimaryKey();
+ $multi = count($primaryKey) > 1;
+ $primaryKey = array_map([$target, 'aliasField'], $primaryKey);
+
+ if ($multi) {
+ $first = current($ids);
+ if (!is_array($first) || count($first) !== count($primaryKey)) {
+ return [];
+ }
+ $type = [];
+ $schema = $target->getSchema();
+ foreach ((array)$target->getPrimaryKey() as $column) {
+ $type[] = $schema->getColumnType($column);
+ }
+ $filter = new TupleComparison($primaryKey, $ids, $type, 'IN');
+ } else {
+ $filter = [$primaryKey[0] . ' IN' => $ids];
+ }
+
+ return $target->find()->where($filter)->toArray();
+ }
+
+ /**
+ * Merges `$data` into `$entity` and recursively does the same for each one of
+ * the association names passed in `$options`. When merging associations, if an
+ * entity is not present in the parent entity for a given association, a new one
+ * will be created.
+ *
+ * When merging HasMany or BelongsToMany associations, all the entities in the
+ * `$data` array will appear, those that can be matched by primary key will get
+ * the data merged, but those that cannot, will be discarded. `ids` option can be used
+ * to determine whether the association must use the `_ids` format.
+ *
+ * ### Options:
+ *
+ * - associated: Associations listed here will be marshalled as well.
+ * - validate: Whether or not to validate data before hydrating the entities. Can
+ * also be set to a string to use a specific validator. Defaults to true/default.
+ * - fields: An allowed list of fields to be assigned to the entity. If not present
+ * the accessible fields list in the entity will be used.
+ * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
+ *
+ * The above options can be used in each nested `associated` array. In addition to the above
+ * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations.
+ * When true this option restricts the request data to only be read from `_ids`.
+ *
+ * ```
+ * $result = $marshaller->merge($entity, $data, [
+ * 'associated' => ['Tags' => ['onlyIds' => true]]
+ * ]);
+ * ```
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
+ * data merged in
+ * @param array $data key value list of fields to be merged into the entity
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface
+ * @see \Cake\ORM\Entity::$_accessible
+ */
+ public function merge(EntityInterface $entity, array $data, array $options = []): EntityInterface
+ {
+ [$data, $options] = $this->_prepareDataAndOptions($data, $options);
+
+ $isNew = $entity->isNew();
+ $keys = [];
+
+ if (!$isNew) {
+ $keys = $entity->extract((array)$this->_table->getPrimaryKey());
+ }
+
+ if (isset($options['accessibleFields'])) {
+ foreach ((array)$options['accessibleFields'] as $key => $value) {
+ $entity->setAccess($key, $value);
+ }
+ }
+
+ $errors = $this->_validate($data + $keys, $options, $isNew);
+ $options['isMerge'] = true;
+ $propertyMap = $this->_buildPropertyMap($data, $options);
+ $properties = [];
+ foreach ($data as $key => $value) {
+ if (!empty($errors[$key])) {
+ if ($entity instanceof InvalidPropertyInterface) {
+ $entity->setInvalidField($key, $value);
+ }
+ continue;
+ }
+ $original = $entity->get($key);
+
+ if (isset($propertyMap[$key])) {
+ $value = $propertyMap[$key]($value, $entity);
+
+ // Don't dirty scalar values and objects that didn't
+ // change. Arrays will always be marked as dirty because
+ // the original/updated list could contain references to the
+ // same objects, even though those objects may have changed internally.
+ if (
+ (
+ is_scalar($value)
+ && $original === $value
+ )
+ || (
+ $value === null
+ && $original === $value
+ )
+ || (
+ is_object($value)
+ && !($value instanceof EntityInterface)
+ && $original == $value
+ )
+ ) {
+ continue;
+ }
+ }
+ $properties[$key] = $value;
+ }
+
+ $entity->setErrors($errors);
+ if (!isset($options['fields'])) {
+ $entity->set($properties);
+
+ foreach ($properties as $field => $value) {
+ if ($value instanceof EntityInterface) {
+ $entity->setDirty($field, $value->isDirty());
+ }
+ }
+ $this->dispatchAfterMarshal($entity, $data, $options);
+
+ return $entity;
+ }
+
+ foreach ((array)$options['fields'] as $field) {
+ if (!array_key_exists($field, $properties)) {
+ continue;
+ }
+ $entity->set($field, $properties[$field]);
+ if ($properties[$field] instanceof EntityInterface) {
+ $entity->setDirty($field, $properties[$field]->isDirty());
+ }
+ }
+ $this->dispatchAfterMarshal($entity, $data, $options);
+
+ return $entity;
+ }
+
+ /**
+ * Merges each of the elements from `$data` into each of the entities in `$entities`
+ * and recursively does the same for each of the association names passed in
+ * `$options`. When merging associations, if an entity is not present in the parent
+ * entity for a given association, a new one will be created.
+ *
+ * Records in `$data` are matched against the entities using the primary key
+ * column. Entries in `$entities` that cannot be matched to any record in
+ * `$data` will be discarded. Records in `$data` that could not be matched will
+ * be marshalled as a new entity.
+ *
+ * When merging HasMany or BelongsToMany associations, all the entities in the
+ * `$data` array will appear, those that can be matched by primary key will get
+ * the data merged, but those that cannot, will be discarded.
+ *
+ * ### Options:
+ *
+ * - validate: Whether or not to validate data before hydrating the entities. Can
+ * also be set to a string to use a specific validator. Defaults to true/default.
+ * - associated: Associations listed here will be marshalled as well.
+ * - fields: An allowed list of fields to be assigned to the entity. If not present,
+ * the accessible fields list in the entity will be used.
+ * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
+ *
+ * @param iterable<\Cake\Datasource\EntityInterface> $entities the entities that will get the
+ * data merged in
+ * @param array $data list of arrays to be merged into the entities
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface[]
+ * @see \Cake\ORM\Entity::$_accessible
+ * @psalm-suppress NullArrayOffset
+ */
+ public function mergeMany(iterable $entities, array $data, array $options = []): array
+ {
+ $primary = (array)$this->_table->getPrimaryKey();
+
+ $indexed = (new Collection($data))
+ ->groupBy(function ($el) use ($primary) {
+ $keys = [];
+ foreach ($primary as $key) {
+ $keys[] = $el[$key] ?? '';
+ }
+
+ return implode(';', $keys);
+ })
+ ->map(function ($element, $key) {
+ return $key === '' ? $element : $element[0];
+ })
+ ->toArray();
+
+ /** @psalm-suppress InvalidArrayOffset */
+ $new = $indexed[null] ?? [];
+ /** @psalm-suppress InvalidArrayOffset */
+ unset($indexed[null]);
+ $output = [];
+
+ foreach ($entities as $entity) {
+ if (!($entity instanceof EntityInterface)) {
+ continue;
+ }
+
+ $key = implode(';', $entity->extract($primary));
+ if (!isset($indexed[$key])) {
+ continue;
+ }
+
+ $output[] = $this->merge($entity, $indexed[$key], $options);
+ unset($indexed[$key]);
+ }
+
+ $conditions = (new Collection($indexed))
+ ->map(function ($data, $key) {
+ return explode(';', (string)$key);
+ })
+ ->filter(function ($keys) use ($primary) {
+ return count(array_filter($keys, 'strlen')) === count($primary);
+ })
+ ->reduce(function ($conditions, $keys) use ($primary) {
+ $fields = array_map([$this->_table, 'aliasField'], $primary);
+ $conditions['OR'][] = array_combine($fields, $keys);
+
+ return $conditions;
+ }, ['OR' => []]);
+ $maybeExistentQuery = $this->_table->find()->where($conditions);
+
+ if (!empty($indexed) && count($maybeExistentQuery->clause('where'))) {
+ foreach ($maybeExistentQuery as $entity) {
+ $key = implode(';', $entity->extract($primary));
+ if (isset($indexed[$key])) {
+ $output[] = $this->merge($entity, $indexed[$key], $options);
+ unset($indexed[$key]);
+ }
+ }
+ }
+
+ foreach ((new Collection($indexed))->append($new) as $value) {
+ if (!is_array($value)) {
+ continue;
+ }
+ $output[] = $this->one($value, $options);
+ }
+
+ return $output;
+ }
+
+ /**
+ * Creates a new sub-marshaller and merges the associated data.
+ *
+ * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $original The original entity
+ * @param \Cake\ORM\Association $assoc The association to merge
+ * @param mixed $value The array of data to hydrate. If not an array, this method will return null.
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
+ */
+ protected function _mergeAssociation($original, Association $assoc, $value, array $options)
+ {
+ if (!$original) {
+ return $this->_marshalAssociation($assoc, $value, $options);
+ }
+ if (!is_array($value)) {
+ return null;
+ }
+
+ $targetTable = $assoc->getTarget();
+ $marshaller = $targetTable->marshaller();
+ $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
+ $type = $assoc->type();
+ if (in_array($type, $types, true)) {
+ /** @psalm-suppress PossiblyInvalidArgument, ArgumentTypeCoercion */
+ return $marshaller->merge($original, $value, $options);
+ }
+ if ($type === Association::MANY_TO_MANY) {
+ /** @psalm-suppress PossiblyInvalidArgument, ArgumentTypeCoercion */
+ return $marshaller->_mergeBelongsToMany($original, $assoc, $value, $options);
+ }
+
+ if ($type === Association::ONE_TO_MANY) {
+ $hasIds = array_key_exists('_ids', $value);
+ $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
+ if ($hasIds && is_array($value['_ids'])) {
+ return $this->_loadAssociatedByIds($assoc, $value['_ids']);
+ }
+ if ($hasIds || $onlyIds) {
+ return [];
+ }
+ }
+
+ /** @psalm-suppress PossiblyInvalidArgument */
+ return $marshaller->mergeMany($original, $value, $options);
+ }
+
+ /**
+ * Creates a new sub-marshaller and merges the associated data for a BelongstoMany
+ * association.
+ *
+ * @param \Cake\Datasource\EntityInterface[] $original The original entities list.
+ * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshall
+ * @param array $value The data to hydrate
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface[]
+ */
+ protected function _mergeBelongsToMany(array $original, BelongsToMany $assoc, array $value, array $options): array
+ {
+ $associated = $options['associated'] ?? [];
+
+ $hasIds = array_key_exists('_ids', $value);
+ $onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
+
+ if ($hasIds && is_array($value['_ids'])) {
+ return $this->_loadAssociatedByIds($assoc, $value['_ids']);
+ }
+ if ($hasIds || $onlyIds) {
+ return [];
+ }
+
+ if (!empty($associated) && !in_array('_joinData', $associated, true) && !isset($associated['_joinData'])) {
+ return $this->mergeMany($original, $value, $options);
+ }
+
+ return $this->_mergeJoinData($original, $assoc, $value, $options);
+ }
+
+ /**
+ * Merge the special _joinData property into the entity set.
+ *
+ * @param \Cake\Datasource\EntityInterface[] $original The original entities list.
+ * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshall
+ * @param array $value The data to hydrate
+ * @param array $options List of options.
+ * @return \Cake\Datasource\EntityInterface[] An array of entities
+ */
+ protected function _mergeJoinData(array $original, BelongsToMany $assoc, array $value, array $options): array
+ {
+ $associated = $options['associated'] ?? [];
+ $extra = [];
+ foreach ($original as $entity) {
+ // Mark joinData as accessible so we can marshal it properly.
+ $entity->setAccess('_joinData', true);
+
+ $joinData = $entity->get('_joinData');
+ if ($joinData && $joinData instanceof EntityInterface) {
+ $extra[spl_object_hash($entity)] = $joinData;
+ }
+ }
+
+ $joint = $assoc->junction();
+ $marshaller = $joint->marshaller();
+
+ $nested = [];
+ if (isset($associated['_joinData'])) {
+ $nested = (array)$associated['_joinData'];
+ }
+
+ $options['accessibleFields'] = ['_joinData' => true];
+
+ $records = $this->mergeMany($original, $value, $options);
+ foreach ($records as $record) {
+ $hash = spl_object_hash($record);
+ $value = $record->get('_joinData');
+
+ // Already an entity, no further marshalling required.
+ if ($value instanceof EntityInterface) {
+ continue;
+ }
+
+ // Scalar data can't be handled
+ if (!is_array($value)) {
+ $record->unset('_joinData');
+ continue;
+ }
+
+ // Marshal data into the old object, or make a new joinData object.
+ if (isset($extra[$hash])) {
+ $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested));
+ } else {
+ $joinData = $marshaller->one($value, $nested);
+ $record->set('_joinData', $joinData);
+ }
+ }
+
+ return $records;
+ }
+
+ /**
+ * dispatch Model.afterMarshal event.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity that was marshaled.
+ * @param array $data readOnly $data to use.
+ * @param array $options List of options that are readOnly.
+ * @return void
+ */
+ protected function dispatchAfterMarshal(EntityInterface $entity, array $data, array $options = []): void
+ {
+ $data = new ArrayObject($data);
+ $options = new ArrayObject($options);
+ $this->_table->dispatchEvent('Model.afterMarshal', compact('entity', 'data', 'options'));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/PropertyMarshalInterface.php b/app/vendor/cakephp/cakephp/src/ORM/PropertyMarshalInterface.php
new file mode 100644
index 000000000..f52ce7494
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/PropertyMarshalInterface.php
@@ -0,0 +1,36 @@
+ callable]` of additional properties to marshal.
+ */
+ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array;
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Query.php b/app/vendor/cakephp/cakephp/src/ORM/Query.php
new file mode 100644
index 000000000..101723b82
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Query.php
@@ -0,0 +1,1418 @@
+repository($table);
+
+ if ($this->_repository !== null) {
+ $this->addDefaultTypes($this->_repository);
+ }
+ }
+
+ /**
+ * Adds new fields to be returned by a `SELECT` statement when this query is
+ * executed. Fields can be passed as an array of strings, array of expression
+ * objects, a single expression or a single string.
+ *
+ * If an array is passed, keys will be used to alias fields using the value as the
+ * real field to be aliased. It is possible to alias strings, Expression objects or
+ * even other Query objects.
+ *
+ * If a callable function is passed, the returning array of the function will
+ * be used as the list of fields.
+ *
+ * By default this function will append any passed argument to the list of fields
+ * to be selected, unless the second argument is set to true.
+ *
+ * ### Examples:
+ *
+ * ```
+ * $query->select(['id', 'title']); // Produces SELECT id, title
+ * $query->select(['author' => 'author_id']); // Appends author: SELECT id, title, author_id as author
+ * $query->select('id', true); // Resets the list: SELECT id
+ * $query->select(['total' => $countQuery]); // SELECT id, (SELECT ...) AS total
+ * $query->select(function ($query) {
+ * return ['article_id', 'total' => $query->count('*')];
+ * })
+ * ```
+ *
+ * By default no fields are selected, if you have an instance of `Cake\ORM\Query` and try to append
+ * fields you should also call `Cake\ORM\Query::enableAutoFields()` to select the default fields
+ * from the table.
+ *
+ * If you pass an instance of a `Cake\ORM\Table` or `Cake\ORM\Association` class,
+ * all the fields in the schema of the table or the association will be added to
+ * the select clause.
+ *
+ * @param array|\Cake\Database\ExpressionInterface|callable|string|\Cake\ORM\Table|\Cake\ORM\Association $fields Fields
+ * to be added to the list.
+ * @param bool $overwrite whether to reset fields with passed list or not
+ * @return $this
+ */
+ public function select($fields = [], bool $overwrite = false)
+ {
+ if ($fields instanceof Association) {
+ $fields = $fields->getTarget();
+ }
+
+ if ($fields instanceof Table) {
+ if ($this->aliasingEnabled) {
+ $fields = $this->aliasFields($fields->getSchema()->columns(), $fields->getAlias());
+ } else {
+ $fields = $fields->getSchema()->columns();
+ }
+ }
+
+ return parent::select($fields, $overwrite);
+ }
+
+ /**
+ * All the fields associated with the passed table except the excluded
+ * fields will be added to the select clause of the query. Passed excluded fields should not be aliased.
+ * After the first call to this method, a second call cannot be used to remove fields that have already
+ * been added to the query by the first. If you need to change the list after the first call,
+ * pass overwrite boolean true which will reset the select clause removing all previous additions.
+ *
+ * @param \Cake\ORM\Table|\Cake\ORM\Association $table The table to use to get an array of columns
+ * @param string[] $excludedFields The un-aliased column names you do not want selected from $table
+ * @param bool $overwrite Whether to reset/remove previous selected fields
+ * @return $this
+ * @throws \InvalidArgumentException If Association|Table is not passed in first argument
+ */
+ public function selectAllExcept($table, array $excludedFields, bool $overwrite = false)
+ {
+ if ($table instanceof Association) {
+ $table = $table->getTarget();
+ }
+
+ if (!($table instanceof Table)) {
+ throw new InvalidArgumentException('You must provide either an Association or a Table object');
+ }
+
+ $fields = array_diff($table->getSchema()->columns(), $excludedFields);
+ if ($this->aliasingEnabled) {
+ $fields = $this->aliasFields($fields);
+ }
+
+ return $this->select($fields, $overwrite);
+ }
+
+ /**
+ * Hints this object to associate the correct types when casting conditions
+ * for the database. This is done by extracting the field types from the schema
+ * associated to the passed table object. This prevents the user from repeating
+ * themselves when specifying conditions.
+ *
+ * This method returns the same query object for chaining.
+ *
+ * @param \Cake\ORM\Table $table The table to pull types from
+ * @return $this
+ */
+ public function addDefaultTypes(Table $table)
+ {
+ $alias = $table->getAlias();
+ $map = $table->getSchema()->typeMap();
+ $fields = [];
+ foreach ($map as $f => $type) {
+ $fields[$f] = $fields[$alias . '.' . $f] = $fields[$alias . '__' . $f] = $type;
+ }
+ $this->getTypeMap()->addDefaults($fields);
+
+ return $this;
+ }
+
+ /**
+ * Sets the instance of the eager loader class to use for loading associations
+ * and storing containments.
+ *
+ * @param \Cake\ORM\EagerLoader $instance The eager loader to use.
+ * @return $this
+ */
+ public function setEagerLoader(EagerLoader $instance)
+ {
+ $this->_eagerLoader = $instance;
+
+ return $this;
+ }
+
+ /**
+ * Returns the currently configured instance.
+ *
+ * @return \Cake\ORM\EagerLoader
+ */
+ public function getEagerLoader(): EagerLoader
+ {
+ if ($this->_eagerLoader === null) {
+ $this->_eagerLoader = new EagerLoader();
+ }
+
+ return $this->_eagerLoader;
+ }
+
+ /**
+ * Sets the list of associations that should be eagerly loaded along with this
+ * query. The list of associated tables passed must have been previously set as
+ * associations using the Table API.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring articles' author information
+ * $query->contain('Author');
+ *
+ * // Also bring the category and tags associated to each article
+ * $query->contain(['Category', 'Tag']);
+ * ```
+ *
+ * Associations can be arbitrarily nested using dot notation or nested arrays,
+ * this allows this object to calculate joins or any additional queries that
+ * must be executed to bring the required associated data.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Eager load the product info, and for each product load other 2 associations
+ * $query->contain(['Product' => ['Manufacturer', 'Distributor']);
+ *
+ * // Which is equivalent to calling
+ * $query->contain(['Products.Manufactures', 'Products.Distributors']);
+ *
+ * // For an author query, load his region, state and country
+ * $query->contain('Regions.States.Countries');
+ * ```
+ *
+ * It is possible to control the conditions and fields selected for each of the
+ * contained associations:
+ *
+ * ### Example:
+ *
+ * ```
+ * $query->contain(['Tags' => function ($q) {
+ * return $q->where(['Tags.is_popular' => true]);
+ * }]);
+ *
+ * $query->contain(['Products.Manufactures' => function ($q) {
+ * return $q->select(['name'])->where(['Manufactures.active' => true]);
+ * }]);
+ * ```
+ *
+ * Each association might define special options when eager loaded, the allowed
+ * options that can be set per association are:
+ *
+ * - `foreignKey`: Used to set a different field to match both tables, if set to false
+ * no join conditions will be generated automatically. `false` can only be used on
+ * joinable associations and cannot be used with hasMany or belongsToMany associations.
+ * - `fields`: An array with the fields that should be fetched from the association.
+ * - `finder`: The finder to use when loading associated records. Either the name of the
+ * finder as a string, or an array to define options to pass to the finder.
+ * - `queryBuilder`: Equivalent to passing a callable instead of an options array.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Set options for the hasMany articles that will be eagerly loaded for an author
+ * $query->contain([
+ * 'Articles' => [
+ * 'fields' => ['title', 'author_id']
+ * ]
+ * ]);
+ * ```
+ *
+ * Finders can be configured to use options.
+ *
+ * ```
+ * // Retrieve translations for the articles, but only those for the `en` and `es` locales
+ * $query->contain([
+ * 'Articles' => [
+ * 'finder' => [
+ * 'translations' => [
+ * 'locales' => ['en', 'es']
+ * ]
+ * ]
+ * ]
+ * ]);
+ * ```
+ *
+ * When containing associations, it is important to include foreign key columns.
+ * Failing to do so will trigger exceptions.
+ *
+ * ```
+ * // Use a query builder to add conditions to the containment
+ * $query->contain('Authors', function ($q) {
+ * return $q->where(...); // add conditions
+ * });
+ * // Use special join conditions for multiple containments in the same method call
+ * $query->contain([
+ * 'Authors' => [
+ * 'foreignKey' => false,
+ * 'queryBuilder' => function ($q) {
+ * return $q->where(...); // Add full filtering conditions
+ * }
+ * ],
+ * 'Tags' => function ($q) {
+ * return $q->where(...); // add conditions
+ * }
+ * ]);
+ * ```
+ *
+ * If called with an empty first argument and `$override` is set to true, the
+ * previous list will be emptied.
+ *
+ * @param array|string $associations List of table aliases to be queried.
+ * @param callable|bool $override The query builder for the association, or
+ * if associations is an array, a bool on whether to override previous list
+ * with the one passed
+ * defaults to merging previous list with the new one.
+ * @return $this
+ */
+ public function contain($associations, $override = false)
+ {
+ $loader = $this->getEagerLoader();
+ if ($override === true) {
+ $this->clearContain();
+ }
+
+ $queryBuilder = null;
+ if (is_callable($override)) {
+ $queryBuilder = $override;
+ }
+
+ if ($associations) {
+ $loader->contain($associations, $queryBuilder);
+ }
+ $this->_addAssociationsToTypeMap(
+ $this->getRepository(),
+ $this->getTypeMap(),
+ $loader->getContain()
+ );
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getContain(): array
+ {
+ return $this->getEagerLoader()->getContain();
+ }
+
+ /**
+ * Clears the contained associations from the current query.
+ *
+ * @return $this
+ */
+ public function clearContain()
+ {
+ $this->getEagerLoader()->clearContain();
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Used to recursively add contained association column types to
+ * the query.
+ *
+ * @param \Cake\ORM\Table $table The table instance to pluck associations from.
+ * @param \Cake\Database\TypeMap $typeMap The typemap to check for columns in.
+ * This typemap is indirectly mutated via Cake\ORM\Query::addDefaultTypes()
+ * @param array $associations The nested tree of associations to walk.
+ * @return void
+ */
+ protected function _addAssociationsToTypeMap(Table $table, TypeMap $typeMap, array $associations): void
+ {
+ foreach ($associations as $name => $nested) {
+ if (!$table->hasAssociation($name)) {
+ continue;
+ }
+ $association = $table->getAssociation($name);
+ $target = $association->getTarget();
+ $primary = (array)$target->getPrimaryKey();
+ if (empty($primary) || $typeMap->type($target->aliasField($primary[0])) === null) {
+ $this->addDefaultTypes($target);
+ }
+ if (!empty($nested)) {
+ $this->_addAssociationsToTypeMap($target, $typeMap, $nested);
+ }
+ }
+ }
+
+ /**
+ * Adds filtering conditions to this query to only bring rows that have a relation
+ * to another from an associated table, based on conditions in the associated table.
+ *
+ * This function will add entries in the `contain` graph.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring only articles that were tagged with 'cake'
+ * $query->matching('Tags', function ($q) {
+ * return $q->where(['name' => 'cake']);
+ * });
+ * ```
+ *
+ * It is possible to filter by deep associations by using dot notation:
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring only articles that were commented by 'markstory'
+ * $query->matching('Comments.Users', function ($q) {
+ * return $q->where(['username' => 'markstory']);
+ * });
+ * ```
+ *
+ * As this function will create `INNER JOIN`, you might want to consider
+ * calling `distinct` on this query as you might get duplicate rows if
+ * your conditions don't filter them already. This might be the case, for example,
+ * of the same user commenting more than once in the same article.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring unique articles that were commented by 'markstory'
+ * $query->distinct(['Articles.id'])
+ * ->matching('Comments.Users', function ($q) {
+ * return $q->where(['username' => 'markstory']);
+ * });
+ * ```
+ *
+ * Please note that the query passed to the closure will only accept calling
+ * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
+ * add more complex clauses you can do it directly in the main query.
+ *
+ * @param string $assoc The association to filter by
+ * @param callable|null $builder a function that will receive a pre-made query object
+ * that can be used to add custom conditions or selecting some fields
+ * @return $this
+ */
+ public function matching(string $assoc, ?callable $builder = null)
+ {
+ $result = $this->getEagerLoader()->setMatching($assoc, $builder)->getMatching();
+ $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result);
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Creates a LEFT JOIN with the passed association table while preserving
+ * the foreign key matching and the custom conditions that were originally set
+ * for it.
+ *
+ * This function will add entries in the `contain` graph.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Get the count of articles per user
+ * $usersQuery
+ * ->select(['total_articles' => $query->func()->count('Articles.id')])
+ * ->leftJoinWith('Articles')
+ * ->group(['Users.id'])
+ * ->enableAutoFields();
+ * ```
+ *
+ * You can also customize the conditions passed to the LEFT JOIN:
+ *
+ * ```
+ * // Get the count of articles per user with at least 5 votes
+ * $usersQuery
+ * ->select(['total_articles' => $query->func()->count('Articles.id')])
+ * ->leftJoinWith('Articles', function ($q) {
+ * return $q->where(['Articles.votes >=' => 5]);
+ * })
+ * ->group(['Users.id'])
+ * ->enableAutoFields();
+ * ```
+ *
+ * This will create the following SQL:
+ *
+ * ```
+ * SELECT COUNT(Articles.id) AS total_articles, Users.*
+ * FROM users Users
+ * LEFT JOIN articles Articles ON Articles.user_id = Users.id AND Articles.votes >= 5
+ * GROUP BY USers.id
+ * ```
+ *
+ * It is possible to left join deep associations by using dot notation
+ *
+ * ### Example:
+ *
+ * ```
+ * // Total comments in articles by 'markstory'
+ * $query
+ * ->select(['total_comments' => $query->func()->count('Comments.id')])
+ * ->leftJoinWith('Comments.Users', function ($q) {
+ * return $q->where(['username' => 'markstory']);
+ * })
+ * ->group(['Users.id']);
+ * ```
+ *
+ * Please note that the query passed to the closure will only accept calling
+ * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
+ * add more complex clauses you can do it directly in the main query.
+ *
+ * @param string $assoc The association to join with
+ * @param callable|null $builder a function that will receive a pre-made query object
+ * that can be used to add custom conditions or selecting some fields
+ * @return $this
+ */
+ public function leftJoinWith(string $assoc, ?callable $builder = null)
+ {
+ $result = $this->getEagerLoader()
+ ->setMatching($assoc, $builder, [
+ 'joinType' => Query::JOIN_TYPE_LEFT,
+ 'fields' => false,
+ ])
+ ->getMatching();
+ $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result);
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Creates an INNER JOIN with the passed association table while preserving
+ * the foreign key matching and the custom conditions that were originally set
+ * for it.
+ *
+ * This function will add entries in the `contain` graph.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring only articles that were tagged with 'cake'
+ * $query->innerJoinWith('Tags', function ($q) {
+ * return $q->where(['name' => 'cake']);
+ * });
+ * ```
+ *
+ * This will create the following SQL:
+ *
+ * ```
+ * SELECT Articles.*
+ * FROM articles Articles
+ * INNER JOIN tags Tags ON Tags.name = 'cake'
+ * INNER JOIN articles_tags ArticlesTags ON ArticlesTags.tag_id = Tags.id
+ * AND ArticlesTags.articles_id = Articles.id
+ * ```
+ *
+ * This function works the same as `matching()` with the difference that it
+ * will select no fields from the association.
+ *
+ * @param string $assoc The association to join with
+ * @param callable|null $builder a function that will receive a pre-made query object
+ * that can be used to add custom conditions or selecting some fields
+ * @return $this
+ * @see \Cake\ORM\Query::matching()
+ */
+ public function innerJoinWith(string $assoc, ?callable $builder = null)
+ {
+ $result = $this->getEagerLoader()
+ ->setMatching($assoc, $builder, [
+ 'joinType' => Query::JOIN_TYPE_INNER,
+ 'fields' => false,
+ ])
+ ->getMatching();
+ $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result);
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Adds filtering conditions to this query to only bring rows that have no match
+ * to another from an associated table, based on conditions in the associated table.
+ *
+ * This function will add entries in the `contain` graph.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring only articles that were not tagged with 'cake'
+ * $query->notMatching('Tags', function ($q) {
+ * return $q->where(['name' => 'cake']);
+ * });
+ * ```
+ *
+ * It is possible to filter by deep associations by using dot notation:
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring only articles that weren't commented by 'markstory'
+ * $query->notMatching('Comments.Users', function ($q) {
+ * return $q->where(['username' => 'markstory']);
+ * });
+ * ```
+ *
+ * As this function will create a `LEFT JOIN`, you might want to consider
+ * calling `distinct` on this query as you might get duplicate rows if
+ * your conditions don't filter them already. This might be the case, for example,
+ * of the same article having multiple comments.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Bring unique articles that were commented by 'markstory'
+ * $query->distinct(['Articles.id'])
+ * ->notMatching('Comments.Users', function ($q) {
+ * return $q->where(['username' => 'markstory']);
+ * });
+ * ```
+ *
+ * Please note that the query passed to the closure will only accept calling
+ * `select`, `where`, `andWhere` and `orWhere` on it. If you wish to
+ * add more complex clauses you can do it directly in the main query.
+ *
+ * @param string $assoc The association to filter by
+ * @param callable|null $builder a function that will receive a pre-made query object
+ * that can be used to add custom conditions or selecting some fields
+ * @return $this
+ */
+ public function notMatching(string $assoc, ?callable $builder = null)
+ {
+ $result = $this->getEagerLoader()
+ ->setMatching($assoc, $builder, [
+ 'joinType' => Query::JOIN_TYPE_LEFT,
+ 'fields' => false,
+ 'negateMatch' => true,
+ ])
+ ->getMatching();
+ $this->_addAssociationsToTypeMap($this->getRepository(), $this->getTypeMap(), $result);
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * Populates or adds parts to current query clauses using an array.
+ * This is handy for passing all query clauses at once. The option array accepts:
+ *
+ * - fields: Maps to the select method
+ * - conditions: Maps to the where method
+ * - limit: Maps to the limit method
+ * - order: Maps to the order method
+ * - offset: Maps to the offset method
+ * - group: Maps to the group method
+ * - having: Maps to the having method
+ * - contain: Maps to the contain options for eager loading
+ * - join: Maps to the join method
+ * - page: Maps to the page method
+ *
+ * ### Example:
+ *
+ * ```
+ * $query->applyOptions([
+ * 'fields' => ['id', 'name'],
+ * 'conditions' => [
+ * 'created >=' => '2013-01-01'
+ * ],
+ * 'limit' => 10
+ * ]);
+ * ```
+ *
+ * Is equivalent to:
+ *
+ * ```
+ * $query
+ * ->select(['id', 'name'])
+ * ->where(['created >=' => '2013-01-01'])
+ * ->limit(10)
+ * ```
+ *
+ * @param array $options the options to be applied
+ * @return $this
+ */
+ public function applyOptions(array $options)
+ {
+ $valid = [
+ 'fields' => 'select',
+ 'conditions' => 'where',
+ 'join' => 'join',
+ 'order' => 'order',
+ 'limit' => 'limit',
+ 'offset' => 'offset',
+ 'group' => 'group',
+ 'having' => 'having',
+ 'contain' => 'contain',
+ 'page' => 'page',
+ ];
+
+ ksort($options);
+ foreach ($options as $option => $values) {
+ if (isset($valid[$option], $values)) {
+ $this->{$valid[$option]}($values);
+ } else {
+ $this->_options[$option] = $values;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Creates a copy of this current query, triggers beforeFind and resets some state.
+ *
+ * The following state will be cleared:
+ *
+ * - autoFields
+ * - limit
+ * - offset
+ * - map/reduce functions
+ * - result formatters
+ * - order
+ * - containments
+ *
+ * This method creates query clones that are useful when working with subqueries.
+ *
+ * @return static
+ */
+ public function cleanCopy()
+ {
+ $clone = clone $this;
+ $clone->triggerBeforeFind();
+ $clone->disableAutoFields();
+ $clone->limit(null);
+ $clone->order([], true);
+ $clone->offset(null);
+ $clone->mapReduce(null, null, true);
+ $clone->formatResults(null, self::OVERWRITE);
+ $clone->setSelectTypeMap(new TypeMap());
+ $clone->decorateResults(null, true);
+
+ return $clone;
+ }
+
+ /**
+ * Clears the internal result cache and the internal count value from the current
+ * query object.
+ *
+ * @return $this
+ */
+ public function clearResult()
+ {
+ $this->_dirty();
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Handles cloning eager loaders.
+ */
+ public function __clone()
+ {
+ parent::__clone();
+ if ($this->_eagerLoader !== null) {
+ $this->_eagerLoader = clone $this->_eagerLoader;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Returns the COUNT(*) for the query. If the query has not been
+ * modified, and the count has already been performed the cached
+ * value is returned
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ if ($this->_resultsCount === null) {
+ $this->_resultsCount = $this->_performCount();
+ }
+
+ return $this->_resultsCount;
+ }
+
+ /**
+ * Performs and returns the COUNT(*) for the query.
+ *
+ * @return int
+ */
+ protected function _performCount(): int
+ {
+ $query = $this->cleanCopy();
+ $counter = $this->_counter;
+ if ($counter !== null) {
+ $query->counter(null);
+
+ return (int)$counter($query);
+ }
+
+ $complex = (
+ $query->clause('distinct') ||
+ count($query->clause('group')) ||
+ count($query->clause('union')) ||
+ $query->clause('having')
+ );
+
+ if (!$complex) {
+ // Expression fields could have bound parameters.
+ foreach ($query->clause('select') as $field) {
+ if ($field instanceof ExpressionInterface) {
+ $complex = true;
+ break;
+ }
+ }
+ }
+
+ if (!$complex && $this->_valueBinder !== null) {
+ $order = $this->clause('order');
+ $complex = $order === null ? false : $order->hasNestedExpression();
+ }
+
+ $count = ['count' => $query->func()->count('*')];
+
+ if (!$complex) {
+ $query->getEagerLoader()->disableAutoFields();
+ $statement = $query
+ ->select($count, true)
+ ->disableAutoFields()
+ ->execute();
+ } else {
+ $statement = $this->getConnection()->newQuery()
+ ->select($count)
+ ->from(['count_source' => $query])
+ ->execute();
+ }
+
+ $result = $statement->fetch('assoc');
+ $statement->closeCursor();
+
+ if ($result === false) {
+ return 0;
+ }
+
+ return (int)$result['count'];
+ }
+
+ /**
+ * Registers a callable function that will be executed when the `count` method in
+ * this query is called. The return value for the function will be set as the
+ * return value of the `count` method.
+ *
+ * This is particularly useful when you need to optimize a query for returning the
+ * count, for example removing unnecessary joins, removing group by or just return
+ * an estimated number of rows.
+ *
+ * The callback will receive as first argument a clone of this query and not this
+ * query itself.
+ *
+ * If the first param is a null value, the built-in counter function will be called
+ * instead
+ *
+ * @param callable|null $counter The counter value
+ * @return $this
+ */
+ public function counter(?callable $counter)
+ {
+ $this->_counter = $counter;
+
+ return $this;
+ }
+
+ /**
+ * Toggle hydrating entities.
+ *
+ * If set to false array results will be returned for the query.
+ *
+ * @param bool $enable Use a boolean to set the hydration mode.
+ * @return $this
+ */
+ public function enableHydration(bool $enable = true)
+ {
+ $this->_dirty();
+ $this->_hydrate = $enable;
+
+ return $this;
+ }
+
+ /**
+ * Disable hydrating entities.
+ *
+ * Disabling hydration will cause array results to be returned for the query
+ * instead of entities.
+ *
+ * @return $this
+ */
+ public function disableHydration()
+ {
+ $this->_dirty();
+ $this->_hydrate = false;
+
+ return $this;
+ }
+
+ /**
+ * Returns the current hydration mode.
+ *
+ * @return bool
+ */
+ public function isHydrationEnabled(): bool
+ {
+ return $this->_hydrate;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param \Closure|string|false $key Either the cache key or a function to generate the cache key.
+ * When using a function, this query instance will be supplied as an argument.
+ * @param string|\Cake\Cache\CacheEngine $config Either the name of the cache config to use, or
+ * a cache config instance.
+ * @return $this
+ * @throws \RuntimeException When you attempt to cache a non-select query.
+ */
+ public function cache($key, $config = 'default')
+ {
+ if ($this->_type !== 'select' && $this->_type !== null) {
+ throw new RuntimeException('You cannot cache the results of non-select queries.');
+ }
+
+ return $this->_cache($key, $config);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return \Cake\Datasource\ResultSetInterface
+ * @throws \RuntimeException if this method is called on a non-select Query.
+ */
+ public function all(): ResultSetInterface
+ {
+ if ($this->_type !== 'select' && $this->_type !== null) {
+ throw new RuntimeException(
+ 'You cannot call all() on a non-select query. Use execute() instead.'
+ );
+ }
+
+ return $this->_all();
+ }
+
+ /**
+ * Trigger the beforeFind event on the query's repository object.
+ *
+ * Will not trigger more than once, and only for select queries.
+ *
+ * @return void
+ */
+ public function triggerBeforeFind(): void
+ {
+ if (!$this->_beforeFindFired && $this->_type === 'select') {
+ $this->_beforeFindFired = true;
+
+ $repository = $this->getRepository();
+ $repository->dispatchEvent('Model.beforeFind', [
+ $this,
+ new ArrayObject($this->_options),
+ !$this->isEagerLoaded(),
+ ]);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sql(?ValueBinder $binder = null): string
+ {
+ $this->triggerBeforeFind();
+
+ $this->_transformQuery();
+
+ return parent::sql($binder);
+ }
+
+ /**
+ * Executes this query and returns a ResultSet object containing the results.
+ * This will also setup the correct statement class in order to eager load deep
+ * associations.
+ *
+ * @return \Cake\Datasource\ResultSetInterface
+ */
+ protected function _execute(): ResultSetInterface
+ {
+ $this->triggerBeforeFind();
+ if ($this->_results) {
+ $decorator = $this->_decoratorClass();
+
+ return new $decorator($this->_results);
+ }
+
+ $statement = $this->getEagerLoader()->loadExternal($this, $this->execute());
+
+ return new ResultSet($this, $statement);
+ }
+
+ /**
+ * Applies some defaults to the query object before it is executed.
+ *
+ * Specifically add the FROM clause, adds default table fields if none are
+ * specified and applies the joins required to eager load associations defined
+ * using `contain`
+ *
+ * It also sets the default types for the columns in the select clause
+ *
+ * @see \Cake\Database\Query::execute()
+ * @return void
+ */
+ protected function _transformQuery(): void
+ {
+ if (!$this->_dirty || $this->_type !== 'select') {
+ return;
+ }
+
+ $repository = $this->getRepository();
+
+ if (empty($this->_parts['from'])) {
+ $this->from([$repository->getAlias() => $repository->getTable()]);
+ }
+ $this->_addDefaultFields();
+ $this->getEagerLoader()->attachAssociations($this, $repository, !$this->_hasFields);
+ $this->_addDefaultSelectTypes();
+ }
+
+ /**
+ * Inspects if there are any set fields for selecting, otherwise adds all
+ * the fields for the default table.
+ *
+ * @return void
+ */
+ protected function _addDefaultFields(): void
+ {
+ $select = $this->clause('select');
+ $this->_hasFields = true;
+
+ $repository = $this->getRepository();
+
+ if (!count($select) || $this->_autoFields === true) {
+ $this->_hasFields = false;
+ $this->select($repository->getSchema()->columns());
+ $select = $this->clause('select');
+ }
+
+ if ($this->aliasingEnabled) {
+ $select = $this->aliasFields($select, $repository->getAlias());
+ }
+ $this->select($select, true);
+ }
+
+ /**
+ * Sets the default types for converting the fields in the select clause
+ *
+ * @return void
+ */
+ protected function _addDefaultSelectTypes(): void
+ {
+ $typeMap = $this->getTypeMap()->getDefaults();
+ $select = $this->clause('select');
+ $types = [];
+
+ foreach ($select as $alias => $value) {
+ if (isset($typeMap[$alias])) {
+ $types[$alias] = $typeMap[$alias];
+ continue;
+ }
+ if (is_string($value) && isset($typeMap[$value])) {
+ $types[$alias] = $typeMap[$value];
+ }
+ if ($value instanceof TypedResultInterface) {
+ $types[$alias] = $value->getReturnType();
+ }
+ }
+ $this->getSelectTypeMap()->addDefaults($types);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $finder The finder method to use.
+ * @param array $options The options for the finder.
+ * @return static Returns a modified query.
+ * @psalm-suppress MoreSpecificReturnType
+ */
+ public function find(string $finder, array $options = [])
+ {
+ $table = $this->getRepository();
+
+ /** @psalm-suppress LessSpecificReturnStatement */
+ return $table->callFinder($finder, $this, $options);
+ }
+
+ /**
+ * Marks a query as dirty, removing any preprocessed information
+ * from in memory caching such as previous results
+ *
+ * @return void
+ */
+ protected function _dirty(): void
+ {
+ $this->_results = null;
+ $this->_resultsCount = null;
+ parent::_dirty();
+ }
+
+ /**
+ * Create an update query.
+ *
+ * This changes the query type to be 'update'.
+ * Can be combined with set() and where() methods to create update queries.
+ *
+ * @param string|\Cake\Database\ExpressionInterface|null $table Unused parameter.
+ * @return $this
+ */
+ public function update($table = null)
+ {
+ if (!$table) {
+ $repository = $this->getRepository();
+ $table = $repository->getTable();
+ }
+
+ return parent::update($table);
+ }
+
+ /**
+ * Create a delete query.
+ *
+ * This changes the query type to be 'delete'.
+ * Can be combined with the where() method to create delete queries.
+ *
+ * @param string|null $table Unused parameter.
+ * @return $this
+ */
+ public function delete(?string $table = null)
+ {
+ $repository = $this->getRepository();
+ $this->from([$repository->getAlias() => $repository->getTable()]);
+
+ // We do not pass $table to parent class here
+ return parent::delete();
+ }
+
+ /**
+ * Create an insert query.
+ *
+ * This changes the query type to be 'insert'.
+ * Note calling this method will reset any data previously set
+ * with Query::values()
+ *
+ * Can be combined with the where() method to create delete queries.
+ *
+ * @param array $columns The columns to insert into.
+ * @param array $types A map between columns & their datatypes.
+ * @return $this
+ */
+ public function insert(array $columns, array $types = [])
+ {
+ $repository = $this->getRepository();
+ $table = $repository->getTable();
+ $this->into($table);
+
+ return parent::insert($columns, $types);
+ }
+
+ /**
+ * Returns a new Query that has automatic field aliasing disabled.
+ *
+ * @param \Cake\ORM\Table $table The table this query is starting on
+ * @return static
+ */
+ public static function subquery(Table $table)
+ {
+ $query = new static($table->getConnection(), $table);
+ $query->aliasingEnabled = false;
+
+ return $query;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $method the method to call
+ * @param array $arguments list of arguments for the method to call
+ * @return mixed
+ * @throws \BadMethodCallException if the method is called for a non-select query
+ */
+ public function __call(string $method, array $arguments)
+ {
+ if ($this->type() === 'select') {
+ return $this->_call($method, $arguments);
+ }
+
+ throw new BadMethodCallException(
+ sprintf('Cannot call method "%s" on a "%s" query', $method, $this->type())
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function __debugInfo(): array
+ {
+ $eagerLoader = $this->getEagerLoader();
+
+ return parent::__debugInfo() + [
+ 'hydrate' => $this->_hydrate,
+ 'buffered' => $this->_useBufferedResults,
+ 'formatters' => count($this->_formatters),
+ 'mapReducers' => count($this->_mapReduce),
+ 'contain' => $eagerLoader->getContain(),
+ 'matching' => $eagerLoader->getMatching(),
+ 'extraOptions' => $this->_options,
+ 'repository' => $this->_repository,
+ ];
+ }
+
+ /**
+ * Executes the query and converts the result set into JSON.
+ *
+ * Part of JsonSerializable interface.
+ *
+ * @return \Cake\Datasource\ResultSetInterface The data to convert to JSON.
+ */
+ public function jsonSerialize(): ResultSetInterface
+ {
+ return $this->all();
+ }
+
+ /**
+ * Sets whether or not the ORM should automatically append fields.
+ *
+ * By default calling select() will disable auto-fields. You can re-enable
+ * auto-fields with this method.
+ *
+ * @param bool $value Set true to enable, false to disable.
+ * @return $this
+ */
+ public function enableAutoFields(bool $value = true)
+ {
+ $this->_autoFields = $value;
+
+ return $this;
+ }
+
+ /**
+ * Disables automatically appending fields.
+ *
+ * @return $this
+ */
+ public function disableAutoFields()
+ {
+ $this->_autoFields = false;
+
+ return $this;
+ }
+
+ /**
+ * Gets whether or not the ORM should automatically append fields.
+ *
+ * By default calling select() will disable auto-fields. You can re-enable
+ * auto-fields with enableAutoFields().
+ *
+ * @return bool|null The current value. Returns null if neither enabled or disabled yet.
+ */
+ public function isAutoFieldsEnabled(): ?bool
+ {
+ return $this->_autoFields;
+ }
+
+ /**
+ * Decorates the results iterator with MapReduce routines and formatters
+ *
+ * @param \Traversable $result Original results
+ * @return \Cake\Datasource\ResultSetInterface
+ */
+ protected function _decorateResults(Traversable $result): ResultSetInterface
+ {
+ $result = $this->_applyDecorators($result);
+
+ if (!($result instanceof ResultSet) && $this->isBufferedResultsEnabled()) {
+ $class = $this->_decoratorClass();
+ $result = new $class($result->buffered());
+ }
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/README.md b/app/vendor/cakephp/cakephp/src/ORM/README.md
new file mode 100644
index 000000000..5af443ac1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/README.md
@@ -0,0 +1,241 @@
+[](https://packagist.org/packages/cakephp/orm)
+[](LICENSE.txt)
+
+# CakePHP ORM
+
+The CakePHP ORM provides a powerful and flexible way to work with relational
+databases. Using a datamapper pattern the ORM allows you to manipulate data as
+entities allowing you to create expressive domain layers in your applications.
+
+## Database engines supported
+
+The CakePHP ORM is compatible with:
+
+* MySQL 5.1+
+* Postgres 8+
+* SQLite3
+* SQLServer 2008+
+* Oracle (through a [community plugin](https://github.com/CakeDC/cakephp-oracle-driver))
+
+## Connecting to the Database
+
+The first thing you need to do when using this library is register a connection
+object. Before performing any operations with the connection, you need to
+specify a driver to use:
+
+```php
+use Cake\Datasource\ConnectionManager;
+
+ConnectionManager::setConfig('default', [
+ 'className' => \Cake\Database\Connection::class,
+ 'driver' => \Cake\Database\Driver\Mysql::class,
+ 'database' => 'test',
+ 'username' => 'root',
+ 'password' => 'secret',
+ 'cacheMetadata' => true,
+ 'quoteIdentifiers' => false,
+]);
+```
+
+Once a 'default' connection is registered, it will be used by all the Table
+mappers if no explicit connection is defined.
+
+## Using Table Locator
+
+In order to access table instances you need to use a *Table Locator*.
+
+```php
+use Cake\ORM\Locator\TableLocator;
+
+$locator = new TableLocator();
+$articles = $locator->get('Articles');
+```
+
+You can also use a trait for easy access to the locator instance:
+
+```php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$articles = $this->getTableLocator()->get('Articles');
+```
+
+By default, classes using `LocatorAwareTrait` will share a global locator instance.
+You can inject your own locator instance into the object:
+
+```php
+use Cake\ORM\Locator\TableLocator;
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$locator = new TableLocator();
+$this->setTableLocator($locator);
+
+$articles = $this->getTableLocator()->get('Articles');
+```
+
+## Creating Associations
+
+In your table classes you can define the relations between your tables. CakePHP's ORM
+supports 4 association types out of the box:
+
+* belongsTo - E.g. Many articles belong to a user.
+* hasOne - E.g. A user has one profile.
+* hasMany - E.g. A user has many articles.
+* belongsToMany - E.g. An article belongsToMany tags.
+
+You define associations in your table's `initialize()` method. See the
+[documentation](https://book.cakephp.org/4/en/orm/associations.html) for
+complete examples.
+
+## Reading Data
+
+Once you've defined some table classes you can read existing data in your tables:
+
+```php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$articles = $this->getTableLocator()->get('Articles');
+foreach ($articles->find() as $article) {
+ echo $article->title;
+}
+```
+
+You can use the [query builder](https://book.cakephp.org/4/en/orm/query-builder.html) to create
+complex queries, and a [variety of methods](https://book.cakephp.org/4/en/orm/retrieving-data-and-resultsets.html)
+to access your data.
+
+## Saving Data
+
+Table objects provide ways to convert request data into entities, and then persist
+those entities to the database:
+
+```php
+use Cake\ORM\Locator\LocatorAwareTrait;
+
+$data = [
+ 'title' => 'My first article',
+ 'body' => 'It is a great article',
+ 'user_id' => 1,
+ 'tags' => [
+ '_ids' => [1, 2, 3]
+ ],
+ 'comments' => [
+ ['comment' => 'Good job'],
+ ['comment' => 'Awesome work'],
+ ]
+];
+
+$articles = $this->getTableLocator()->get('Articles');
+$article = $articles->newEntity($data, [
+ 'associated' => ['Tags', 'Comments']
+]);
+$articles->save($article, [
+ 'associated' => ['Tags', 'Comments']
+])
+```
+
+The above shows how you can easily marshal and save an entity and its
+associations in a simple & powerful way. Consult the [ORM documentation](https://book.cakephp.org/4/en/orm/saving-data.html)
+for more in-depth examples.
+
+## Deleting Data
+
+Once you have a reference to an entity, you can use it to delete data:
+
+```php
+$articles = $this->getTableLocator()->get('Articles');
+$article = $articles->get(2);
+$articles->delete($article);
+```
+
+## Meta Data Cache
+
+It is recommended to enable metadata cache for production systems to avoid performance issues.
+For e.g. file system strategy your bootstrap file could look like this:
+
+```php
+use Cake\Cache\Engine\FileEngine;
+
+$cacheConfig = [
+ 'className' => FileEngine::class,
+ 'duration' => '+1 year',
+ 'serialize' => true,
+ 'prefix' => 'orm_',
+];
+Cache::setConfig('_cake_model_', $cacheConfig);
+```
+
+Cache configs are optional, so you must require ``cachephp/cache`` to add one.
+
+## Creating Custom Table and Entity Classes
+
+By default, the Cake ORM uses the `\Cake\ORM\Table` and `\Cake\ORM\Entity` classes to
+interact with the database. While using the default classes makes sense for
+quick scripts and small applications, you will often want to use your own
+classes for adding your custom logic.
+
+When using the ORM as a standalone package, you are free to choose where to
+store these classes. For example, you could use the `Data` folder for this:
+
+```php
+setEntityClass(Article::class);
+ $this->belongsTo('Users', ['className' => UsersTable::class]);
+ }
+}
+```
+
+This table class is now setup to connect to the `articles` table in your
+database and return instances of `Article` when fetching results. In order to
+get an instance of this class, as shown before, you can use the `TableLocator`:
+
+```php
+get('Articles', ['className' => ArticlesTable::class]);
+```
+
+### Using Conventions-Based Loading
+
+It may get quite tedious having to specify each time the class name to load. So
+the Cake ORM can do most of the work for you if you give it some configuration.
+
+The convention is to have all ORM related classes inside the `src/Model` folder,
+that is the `Model` sub-namespace for your app. So you will usually have the
+`src/Model/Table` and `src/Model/Entity` folders in your project. But first, we
+need to inform Cake of the namespace your application lives in:
+
+```php
+getRepository();
+ $this->_statement = $statement;
+ $this->_driver = $query->getConnection()->getDriver();
+ $this->_defaultTable = $repository;
+ $this->_calculateAssociationMap($query);
+ $this->_hydrate = $query->isHydrationEnabled();
+ $this->_entityClass = $repository->getEntityClass();
+ $this->_useBuffering = $query->isBufferedResultsEnabled();
+ $this->_defaultAlias = $this->_defaultTable->getAlias();
+ $this->_calculateColumnMap($query);
+ $this->_autoFields = $query->isAutoFieldsEnabled();
+
+ if ($this->_useBuffering) {
+ $count = $this->count();
+ $this->_results = new SplFixedArray($count);
+ }
+ }
+
+ /**
+ * Returns the current record in the result iterator
+ *
+ * Part of Iterator interface.
+ *
+ * @return array|object
+ */
+ public function current()
+ {
+ return $this->_current;
+ }
+
+ /**
+ * Returns the key of the current record in the iterator
+ *
+ * Part of Iterator interface.
+ *
+ * @return int
+ */
+ public function key(): int
+ {
+ return $this->_index;
+ }
+
+ /**
+ * Advances the iterator pointer to the next record
+ *
+ * Part of Iterator interface.
+ *
+ * @return void
+ */
+ public function next(): void
+ {
+ $this->_index++;
+ }
+
+ /**
+ * Rewinds a ResultSet.
+ *
+ * Part of Iterator interface.
+ *
+ * @throws \Cake\Database\Exception\DatabaseException
+ * @return void
+ */
+ public function rewind(): void
+ {
+ if ($this->_index === 0) {
+ return;
+ }
+
+ if (!$this->_useBuffering) {
+ $msg = 'You cannot rewind an un-buffered ResultSet. '
+ . 'Use Query::bufferResults() to get a buffered ResultSet.';
+ throw new DatabaseException($msg);
+ }
+
+ $this->_index = 0;
+ }
+
+ /**
+ * Whether there are more results to be fetched from the iterator
+ *
+ * Part of Iterator interface.
+ *
+ * @return bool
+ */
+ public function valid(): bool
+ {
+ if ($this->_useBuffering) {
+ $valid = $this->_index < $this->_count;
+ if ($valid && $this->_results[$this->_index] !== null) {
+ $this->_current = $this->_results[$this->_index];
+
+ return true;
+ }
+ if (!$valid) {
+ return $valid;
+ }
+ }
+
+ $this->_current = $this->_fetchResult();
+ $valid = $this->_current !== false;
+
+ if ($valid && $this->_useBuffering) {
+ $this->_results[$this->_index] = $this->_current;
+ }
+ if (!$valid && $this->_statement !== null) {
+ $this->_statement->closeCursor();
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Get the first record from a result set.
+ *
+ * This method will also close the underlying statement cursor.
+ *
+ * @return array|object|null
+ */
+ public function first()
+ {
+ foreach ($this as $result) {
+ if ($this->_statement !== null && !$this->_useBuffering) {
+ $this->_statement->closeCursor();
+ }
+
+ return $result;
+ }
+
+ return null;
+ }
+
+ /**
+ * Serializes a resultset.
+ *
+ * Part of Serializable interface.
+ *
+ * @return string Serialized object
+ */
+ public function serialize(): string
+ {
+ if (!$this->_useBuffering) {
+ $msg = 'You cannot serialize an un-buffered ResultSet. '
+ . 'Use Query::bufferResults() to get a buffered ResultSet.';
+ throw new DatabaseException($msg);
+ }
+
+ while ($this->valid()) {
+ $this->next();
+ }
+
+ if ($this->_results instanceof SplFixedArray) {
+ return serialize($this->_results->toArray());
+ }
+
+ return serialize($this->_results);
+ }
+
+ /**
+ * Unserializes a resultset.
+ *
+ * Part of Serializable interface.
+ *
+ * @param string $serialized Serialized object
+ * @return void
+ */
+ public function unserialize($serialized)
+ {
+ $results = (array)(unserialize($serialized) ?: []);
+ $this->_results = SplFixedArray::fromArray($results);
+ $this->_useBuffering = true;
+ $this->_count = $this->_results->count();
+ }
+
+ /**
+ * Gives the number of rows in the result set.
+ *
+ * Part of the Countable interface.
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ if ($this->_count !== null) {
+ return $this->_count;
+ }
+ if ($this->_statement !== null) {
+ return $this->_count = $this->_statement->rowCount();
+ }
+
+ if ($this->_results instanceof SplFixedArray) {
+ $this->_count = $this->_results->count();
+ } else {
+ $this->_count = count($this->_results);
+ }
+
+ return $this->_count;
+ }
+
+ /**
+ * Calculates the list of associations that should get eager loaded
+ * when fetching each record
+ *
+ * @param \Cake\ORM\Query $query The query from where to derive the associations
+ * @return void
+ */
+ protected function _calculateAssociationMap(Query $query): void
+ {
+ $map = $query->getEagerLoader()->associationsMap($this->_defaultTable);
+ $this->_matchingMap = (new Collection($map))
+ ->match(['matching' => true])
+ ->indexBy('alias')
+ ->toArray();
+
+ $this->_containMap = (new Collection(array_reverse($map)))
+ ->match(['matching' => false])
+ ->indexBy('nestKey')
+ ->toArray();
+ }
+
+ /**
+ * Creates a map of row keys out of the query select clause that can be
+ * used to hydrate nested result sets more quickly.
+ *
+ * @param \Cake\ORM\Query $query The query from where to derive the column map
+ * @return void
+ */
+ protected function _calculateColumnMap(Query $query): void
+ {
+ $map = [];
+ foreach ($query->clause('select') as $key => $field) {
+ $key = trim($key, '"`[]');
+
+ if (strpos($key, '__') <= 0) {
+ $map[$this->_defaultAlias][$key] = $key;
+ continue;
+ }
+
+ $parts = explode('__', $key, 2);
+ $map[$parts[0]][$key] = $parts[1];
+ }
+
+ foreach ($this->_matchingMap as $alias => $assoc) {
+ if (!isset($map[$alias])) {
+ continue;
+ }
+ $this->_matchingMapColumns[$alias] = $map[$alias];
+ unset($map[$alias]);
+ }
+
+ $this->_map = $map;
+ }
+
+ /**
+ * Helper function to fetch the next result from the statement or
+ * seeded results.
+ *
+ * @return mixed
+ */
+ protected function _fetchResult()
+ {
+ if ($this->_statement === null) {
+ return false;
+ }
+
+ $row = $this->_statement->fetch('assoc');
+ if ($row === false) {
+ return $row;
+ }
+
+ return $this->_groupResult($row);
+ }
+
+ /**
+ * Correctly nests results keys including those coming from associations
+ *
+ * @param array $row Array containing columns and values or false if there is no results
+ * @return array|\Cake\Datasource\EntityInterface Results
+ */
+ protected function _groupResult(array $row)
+ {
+ $defaultAlias = $this->_defaultAlias;
+ $results = $presentAliases = [];
+ $options = [
+ 'useSetters' => false,
+ 'markClean' => true,
+ 'markNew' => false,
+ 'guard' => false,
+ ];
+
+ foreach ($this->_matchingMapColumns as $alias => $keys) {
+ $matching = $this->_matchingMap[$alias];
+ $results['_matchingData'][$alias] = array_combine(
+ $keys,
+ array_intersect_key($row, $keys)
+ );
+ if ($this->_hydrate) {
+ /** @var \Cake\ORM\Table $table */
+ $table = $matching['instance'];
+ $options['source'] = $table->getRegistryAlias();
+ /** @var \Cake\Datasource\EntityInterface $entity */
+ $entity = new $matching['entityClass']($results['_matchingData'][$alias], $options);
+ $results['_matchingData'][$alias] = $entity;
+ }
+ }
+
+ foreach ($this->_map as $table => $keys) {
+ $results[$table] = array_combine($keys, array_intersect_key($row, $keys));
+ $presentAliases[$table] = true;
+ }
+
+ // If the default table is not in the results, set
+ // it to an empty array so that any contained
+ // associations hydrate correctly.
+ if (!isset($results[$defaultAlias])) {
+ $results[$defaultAlias] = [];
+ }
+
+ unset($presentAliases[$defaultAlias]);
+
+ foreach ($this->_containMap as $assoc) {
+ $alias = $assoc['nestKey'];
+
+ if ($assoc['canBeJoined'] && empty($this->_map[$alias])) {
+ continue;
+ }
+
+ /** @var \Cake\ORM\Association $instance */
+ $instance = $assoc['instance'];
+
+ if (!$assoc['canBeJoined'] && !isset($row[$alias])) {
+ $results = $instance->defaultRowValue($results, $assoc['canBeJoined']);
+ continue;
+ }
+
+ if (!$assoc['canBeJoined']) {
+ $results[$alias] = $row[$alias];
+ }
+
+ $target = $instance->getTarget();
+ $options['source'] = $target->getRegistryAlias();
+ unset($presentAliases[$alias]);
+
+ if ($assoc['canBeJoined'] && $this->_autoFields !== false) {
+ $hasData = false;
+ foreach ($results[$alias] as $v) {
+ if ($v !== null && $v !== []) {
+ $hasData = true;
+ break;
+ }
+ }
+
+ if (!$hasData) {
+ $results[$alias] = null;
+ }
+ }
+
+ if ($this->_hydrate && $results[$alias] !== null && $assoc['canBeJoined']) {
+ $entity = new $assoc['entityClass']($results[$alias], $options);
+ $results[$alias] = $entity;
+ }
+
+ $results = $instance->transformRow($results, $alias, $assoc['canBeJoined'], $assoc['targetProperty']);
+ }
+
+ foreach ($presentAliases as $alias => $present) {
+ if (!isset($results[$alias])) {
+ continue;
+ }
+ $results[$defaultAlias][$alias] = $results[$alias];
+ }
+
+ if (isset($results['_matchingData'])) {
+ $results[$defaultAlias]['_matchingData'] = $results['_matchingData'];
+ }
+
+ $options['source'] = $this->_defaultTable->getRegistryAlias();
+ if (isset($results[$defaultAlias])) {
+ $results = $results[$defaultAlias];
+ }
+ if ($this->_hydrate && !($results instanceof EntityInterface)) {
+ $results = new $this->_entityClass($results, $options);
+ }
+
+ return $results;
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [
+ 'items' => $this->toArray(),
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/ExistsIn.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/ExistsIn.php
new file mode 100644
index 000000000..6494b2a5b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/ExistsIn.php
@@ -0,0 +1,168 @@
+ false];
+ $this->_options = $options;
+
+ $this->_fields = (array)$fields;
+ $this->_repository = $repository;
+ }
+
+ /**
+ * Performs the existence check
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields
+ * @param array $options Options passed to the check,
+ * where the `repository` key is required.
+ * @throws \RuntimeException When the rule refers to an undefined association.
+ * @return bool
+ */
+ public function __invoke(EntityInterface $entity, array $options): bool
+ {
+ if (is_string($this->_repository)) {
+ if (!$options['repository']->hasAssociation($this->_repository)) {
+ throw new RuntimeException(sprintf(
+ "ExistsIn rule for '%s' is invalid. '%s' is not associated with '%s'.",
+ implode(', ', $this->_fields),
+ $this->_repository,
+ get_class($options['repository'])
+ ));
+ }
+ $repository = $options['repository']->getAssociation($this->_repository);
+ $this->_repository = $repository;
+ }
+
+ $fields = $this->_fields;
+ $source = $target = $this->_repository;
+ if ($target instanceof Association) {
+ $bindingKey = (array)$target->getBindingKey();
+ $realTarget = $target->getTarget();
+ } else {
+ $bindingKey = (array)$target->getPrimaryKey();
+ $realTarget = $target;
+ }
+
+ if (!empty($options['_sourceTable']) && $realTarget === $options['_sourceTable']) {
+ return true;
+ }
+
+ if (!empty($options['repository'])) {
+ $source = $options['repository'];
+ }
+ if ($source instanceof Association) {
+ $source = $source->getSource();
+ }
+
+ if (!$entity->extract($this->_fields, true)) {
+ return true;
+ }
+
+ if ($this->_fieldsAreNull($entity, $source)) {
+ return true;
+ }
+
+ if ($this->_options['allowNullableNulls']) {
+ $schema = $source->getSchema();
+ foreach ($fields as $i => $field) {
+ if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) {
+ unset($bindingKey[$i], $fields[$i]);
+ }
+ }
+ }
+
+ $primary = array_map(
+ function ($key) use ($target) {
+ return $target->aliasField($key) . ' IS';
+ },
+ $bindingKey
+ );
+ $conditions = array_combine(
+ $primary,
+ $entity->extract($fields)
+ );
+
+ return $target->exists($conditions);
+ }
+
+ /**
+ * Checks whether or not the given entity fields are nullable and null.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to check.
+ * @param \Cake\ORM\Table $source The table to use schema from.
+ * @return bool
+ */
+ protected function _fieldsAreNull(EntityInterface $entity, Table $source): bool
+ {
+ $nulls = 0;
+ $schema = $source->getSchema();
+ foreach ($this->_fields as $field) {
+ if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) {
+ $nulls++;
+ }
+ }
+
+ return $nulls === count($this->_fields);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/IsUnique.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/IsUnique.php
new file mode 100644
index 000000000..24cfeae05
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/IsUnique.php
@@ -0,0 +1,106 @@
+ false,
+ ];
+
+ /**
+ * Constructor.
+ *
+ * ### Options
+ *
+ * - `allowMultipleNulls` Allows any field to have multiple null values. Defaults to false.
+ *
+ * @param string[] $fields The list of fields to check uniqueness for
+ * @param array $options The options for unique checks.
+ */
+ public function __construct(array $fields, array $options = [])
+ {
+ $this->_fields = $fields;
+ $this->_options = $options + $this->_options;
+ }
+
+ /**
+ * Performs the uniqueness check
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields
+ * where the `repository` key is required.
+ * @param array $options Options passed to the check,
+ * @return bool
+ */
+ public function __invoke(EntityInterface $entity, array $options): bool
+ {
+ if (!$entity->extract($this->_fields, true)) {
+ return true;
+ }
+
+ $fields = $entity->extract($this->_fields);
+ if ($this->_options['allowMultipleNulls'] && array_filter($fields, 'is_null')) {
+ return true;
+ }
+
+ $alias = $options['repository']->getAlias();
+ $conditions = $this->_alias($alias, $fields);
+ if ($entity->isNew() === false) {
+ $keys = (array)$options['repository']->getPrimaryKey();
+ $keys = $this->_alias($alias, $entity->extract($keys));
+ if (array_filter($keys, 'strlen')) {
+ $conditions['NOT'] = $keys;
+ }
+ }
+
+ return !$options['repository']->exists($conditions);
+ }
+
+ /**
+ * Add a model alias to all the keys in a set of conditions.
+ *
+ * @param string $alias The alias to add.
+ * @param array $conditions The conditions to alias.
+ * @return array
+ */
+ protected function _alias(string $alias, array $conditions): array
+ {
+ $aliased = [];
+ foreach ($conditions as $key => $value) {
+ $aliased["$alias.$key IS"] = $value;
+ }
+
+ return $aliased;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/LinkConstraint.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/LinkConstraint.php
new file mode 100644
index 000000000..6a25ba239
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/LinkConstraint.php
@@ -0,0 +1,196 @@
+_association = $association;
+ $this->_requiredLinkState = $requiredLinkStatus;
+ }
+
+ /**
+ * Callable handler.
+ *
+ * Performs the actual link check.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity involved in the operation.
+ * @param array $options Options passed from the rules checker.
+ * @return bool Whether the check was successful.
+ */
+ public function __invoke(EntityInterface $entity, array $options): bool
+ {
+ $table = $options['repository'] ?? null;
+ if (!($table instanceof Table)) {
+ throw new \InvalidArgumentException(
+ 'Argument 2 is expected to have a `repository` key that holds an instance of `\Cake\ORM\Table`.'
+ );
+ }
+
+ $association = $this->_association;
+ if (!$association instanceof Association) {
+ $association = $table->getAssociation($association);
+ }
+
+ $count = $this->_countLinks($association, $entity);
+
+ if (
+ (
+ $this->_requiredLinkState === static::STATUS_LINKED &&
+ $count < 1
+ ) ||
+ (
+ $this->_requiredLinkState === static::STATUS_NOT_LINKED &&
+ $count !== 0
+ )
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Alias fields.
+ *
+ * @param array $fields The fields that should be aliased.
+ * @param \Cake\ORM\Table $source The object to use for aliasing.
+ * @return array The aliased fields
+ */
+ protected function _aliasFields(array $fields, Table $source): array
+ {
+ foreach ($fields as $key => $value) {
+ $fields[$key] = $source->aliasField($value);
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Build conditions.
+ *
+ * @param array $fields The condition fields.
+ * @param array $values The condition values.
+ * @return array A conditions array combined from the passed fields and values.
+ */
+ protected function _buildConditions(array $fields, array $values): array
+ {
+ if (count($fields) !== count($values)) {
+ throw new \InvalidArgumentException(sprintf(
+ 'The number of fields is expected to match the number of values, got %d field(s) and %d value(s).',
+ count($fields),
+ count($values)
+ ));
+ }
+
+ return array_combine($fields, $values);
+ }
+
+ /**
+ * Count links.
+ *
+ * @param \Cake\ORM\Association $association The association for which to count links.
+ * @param \Cake\Datasource\EntityInterface $entity The entity involved in the operation.
+ * @return int The number of links.
+ */
+ protected function _countLinks(Association $association, EntityInterface $entity): int
+ {
+ $source = $association->getSource();
+
+ $primaryKey = (array)$source->getPrimaryKey();
+ if (!$entity->has($primaryKey)) {
+ throw new \RuntimeException(sprintf(
+ 'LinkConstraint rule on `%s` requires all primary key values for building the counting ' .
+ 'conditions, expected values for `(%s)`, got `(%s)`.',
+ $source->getAlias(),
+ implode(', ', $primaryKey),
+ implode(', ', $entity->extract($primaryKey))
+ ));
+ }
+
+ $aliasedPrimaryKey = $this->_aliasFields($primaryKey, $source);
+ $conditions = $this->_buildConditions(
+ $aliasedPrimaryKey,
+ $entity->extract($primaryKey)
+ );
+
+ return $source
+ ->find()
+ ->matching($association->getName())
+ ->where($conditions)
+ ->count();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Rule/ValidCount.php b/app/vendor/cakephp/cakephp/src/ORM/Rule/ValidCount.php
new file mode 100644
index 000000000..f7e23521a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Rule/ValidCount.php
@@ -0,0 +1,61 @@
+_field = $field;
+ }
+
+ /**
+ * Performs the count check
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields.
+ * @param array $options Options passed to the check.
+ * @return bool True if successful, else false.
+ */
+ public function __invoke(EntityInterface $entity, array $options): bool
+ {
+ $value = $entity->{$this->_field};
+ if (!is_array($value) && !$value instanceof Countable) {
+ return false;
+ }
+
+ return Validation::comparison(count($value), $options['operator'], $options['count']);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/RulesChecker.php b/app/vendor/cakephp/cakephp/src/ORM/RulesChecker.php
new file mode 100644
index 000000000..561843225
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/RulesChecker.php
@@ -0,0 +1,284 @@
+add($rules->isUnique(['email'], 'The email should be unique'));
+ * ```
+ *
+ * ### Options
+ *
+ * - `allowMultipleNulls` Allows any field to have multiple null values. Defaults to false.
+ *
+ * @param string[] $fields The list of fields to check for uniqueness.
+ * @param string|array|null $message The error message to show in case the rule does not pass. Can
+ * also be an array of options. When an array, the 'message' key can be used to provide a message.
+ * @return \Cake\Datasource\RuleInvoker
+ */
+ public function isUnique(array $fields, $message = null): RuleInvoker
+ {
+ $options = is_array($message) ? $message : ['message' => $message];
+ $message = $options['message'] ?? null;
+ unset($options['message']);
+
+ if (!$message) {
+ if ($this->_useI18n) {
+ $message = __d('cake', 'This value is already in use');
+ } else {
+ $message = 'This value is already in use';
+ }
+ }
+
+ $errorField = current($fields);
+
+ return $this->_addError(new IsUnique($fields, $options), '_isUnique', compact('errorField', 'message'));
+ }
+
+ /**
+ * Returns a callable that can be used as a rule for checking that the values
+ * extracted from the entity to check exist as the primary key in another table.
+ *
+ * This is useful for enforcing foreign key integrity checks.
+ *
+ * ### Example:
+ *
+ * ```
+ * $rules->add($rules->existsIn('author_id', 'Authors', 'Invalid Author'));
+ *
+ * $rules->add($rules->existsIn('site_id', new SitesTable(), 'Invalid Site'));
+ * ```
+ *
+ * Available $options are error 'message' and 'allowNullableNulls' flag.
+ * 'message' sets a custom error message.
+ * Set 'allowNullableNulls' to true to accept composite foreign keys where one or more nullable columns are null.
+ *
+ * @param string|string[] $field The field or list of fields to check for existence by
+ * primary key lookup in the other table.
+ * @param \Cake\ORM\Table|\Cake\ORM\Association|string $table The table name where the fields existence will be checked.
+ * @param string|array|null $message The error message to show in case the rule does not pass. Can
+ * also be an array of options. When an array, the 'message' key can be used to provide a message.
+ * @return \Cake\Datasource\RuleInvoker
+ */
+ public function existsIn($field, $table, $message = null): RuleInvoker
+ {
+ $options = [];
+ if (is_array($message)) {
+ $options = $message + ['message' => null];
+ $message = $options['message'];
+ unset($options['message']);
+ }
+
+ if (!$message) {
+ if ($this->_useI18n) {
+ $message = __d('cake', 'This value does not exist');
+ } else {
+ $message = 'This value does not exist';
+ }
+ }
+
+ $errorField = is_string($field) ? $field : current($field);
+
+ return $this->_addError(new ExistsIn($field, $table, $options), '_existsIn', compact('errorField', 'message'));
+ }
+
+ /**
+ * Validates whether links to the given association exist.
+ *
+ * ### Example:
+ *
+ * ```
+ * $rules->addUpdate($rules->isLinkedTo('Articles', 'article'));
+ * ```
+ *
+ * On a `Comments` table that has a `belongsTo Articles` association, this check would ensure that comments
+ * can only be edited as long as they are associated to an existing article.
+ *
+ * @param \Cake\ORM\Association|string $association The association to check for links.
+ * @param string|null $field The name of the association property. When supplied, this is the name used to set
+ * possible errors. When absent, the name is inferred from `$association`.
+ * @param string|null $message The error message to show in case the rule does not pass.
+ * @return \Cake\Datasource\RuleInvoker
+ * @since 4.0.0
+ */
+ public function isLinkedTo($association, ?string $field = null, ?string $message = null): RuleInvoker
+ {
+ return $this->_addLinkConstraintRule(
+ $association,
+ $field,
+ $message,
+ LinkConstraint::STATUS_LINKED,
+ '_isLinkedTo'
+ );
+ }
+
+ /**
+ * Validates whether links to the given association do not exist.
+ *
+ * ### Example:
+ *
+ * ```
+ * $rules->addDelete($rules->isNotLinkedTo('Comments', 'comments'));
+ * ```
+ *
+ * On a `Articles` table that has a `hasMany Comments` association, this check would ensure that articles
+ * can only be deleted when no associated comments exist.
+ *
+ * @param \Cake\ORM\Association|string $association The association to check for links.
+ * @param string|null $field The name of the association property. When supplied, this is the name used to set
+ * possible errors. When absent, the name is inferred from `$association`.
+ * @param string|null $message The error message to show in case the rule does not pass.
+ * @return \Cake\Datasource\RuleInvoker
+ * @since 4.0.0
+ */
+ public function isNotLinkedTo($association, ?string $field = null, ?string $message = null): RuleInvoker
+ {
+ return $this->_addLinkConstraintRule(
+ $association,
+ $field,
+ $message,
+ LinkConstraint::STATUS_NOT_LINKED,
+ '_isNotLinkedTo'
+ );
+ }
+
+ /**
+ * Adds a link constraint rule.
+ *
+ * @param \Cake\ORM\Association|string $association The association to check for links.
+ * @param string|null $errorField The name of the property to use for setting possible errors. When absent,
+ * the name is inferred from `$association`.
+ * @param string|null $message The error message to show in case the rule does not pass.
+ * @param string $linkStatus The ink status required for the check to pass.
+ * @param string $ruleName The alias/name of the rule.
+ * @return \Cake\Datasource\RuleInvoker
+ * @throws \InvalidArgumentException In case the `$association` argument is of an invalid type.
+ * @since 4.0.0
+ * @see \Cake\ORM\RulesChecker::isLinkedTo()
+ * @see \Cake\ORM\RulesChecker::isNotLinkedTo()
+ * @see \Cake\ORM\Rule\LinkConstraint::STATUS_LINKED
+ * @see \Cake\ORM\Rule\LinkConstraint::STATUS_NOT_LINKED
+ */
+ protected function _addLinkConstraintRule(
+ $association,
+ ?string $errorField,
+ ?string $message,
+ string $linkStatus,
+ string $ruleName
+ ): RuleInvoker {
+ if ($association instanceof Association) {
+ $associationAlias = $association->getName();
+
+ if ($errorField === null) {
+ $errorField = $association->getProperty();
+ }
+ } elseif (is_string($association)) {
+ $associationAlias = $association;
+
+ if ($errorField === null) {
+ $repository = $this->_options['repository'] ?? null;
+ if ($repository instanceof Table) {
+ $association = $repository->getAssociation($association);
+ $errorField = $association->getProperty();
+ } else {
+ $errorField = Inflector::underscore($association);
+ }
+ }
+ } else {
+ throw new \InvalidArgumentException(sprintf(
+ 'Argument 1 is expected to be of type `\Cake\ORM\Association|string`, `%s` given.',
+ getTypeName($association)
+ ));
+ }
+
+ if (!$message) {
+ if ($this->_useI18n) {
+ $message = __d(
+ 'cake',
+ 'Cannot modify row: a constraint for the `{0}` association fails.',
+ $associationAlias
+ );
+ } else {
+ $message = sprintf(
+ 'Cannot modify row: a constraint for the `%s` association fails.',
+ $associationAlias
+ );
+ }
+ }
+
+ $rule = new LinkConstraint(
+ $association,
+ $linkStatus
+ );
+
+ return $this->_addError($rule, $ruleName, compact('errorField', 'message'));
+ }
+
+ /**
+ * Validates the count of associated records.
+ *
+ * @param string $field The field to check the count on.
+ * @param int $count The expected count.
+ * @param string $operator The operator for the count comparison.
+ * @param string|null $message The error message to show in case the rule does not pass.
+ * @return \Cake\Datasource\RuleInvoker
+ */
+ public function validCount(
+ string $field,
+ int $count = 0,
+ string $operator = '>',
+ ?string $message = null
+ ): RuleInvoker {
+ if (!$message) {
+ if ($this->_useI18n) {
+ $message = __d('cake', 'The count does not match {0}{1}', [$operator, $count]);
+ } else {
+ $message = sprintf('The count does not match %s%d', $operator, $count);
+ }
+ }
+
+ $errorField = $field;
+
+ return $this->_addError(
+ new ValidCount($field),
+ '_validCount',
+ compact('count', 'operator', 'errorField', 'message')
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/SaveOptionsBuilder.php b/app/vendor/cakephp/cakephp/src/ORM/SaveOptionsBuilder.php
new file mode 100644
index 000000000..12270d58e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/SaveOptionsBuilder.php
@@ -0,0 +1,226 @@
+_table = $table;
+ $this->parseArrayOptions($options);
+
+ parent::__construct();
+ }
+
+ /**
+ * Takes an options array and populates the option object with the data.
+ *
+ * This can be used to turn an options array into the object.
+ *
+ * @throws \InvalidArgumentException If a given option key does not exist.
+ * @param array $array Options array.
+ * @return $this
+ */
+ public function parseArrayOptions(array $array)
+ {
+ foreach ($array as $key => $value) {
+ $this->{$key}($value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set associated options.
+ *
+ * @param string|array $associated String or array of associations.
+ * @return $this
+ */
+ public function associated($associated)
+ {
+ $associated = $this->_normalizeAssociations($associated);
+ $this->_associated($this->_table, $associated);
+ $this->_options['associated'] = $associated;
+
+ return $this;
+ }
+
+ /**
+ * Checks that the associations exists recursively.
+ *
+ * @param \Cake\ORM\Table $table Table object.
+ * @param array $associations An associations array.
+ * @return void
+ */
+ protected function _associated(Table $table, array $associations): void
+ {
+ foreach ($associations as $key => $associated) {
+ if (is_int($key)) {
+ $this->_checkAssociation($table, $associated);
+ continue;
+ }
+ $this->_checkAssociation($table, $key);
+ if (isset($associated['associated'])) {
+ $this->_associated($table->getAssociation($key)->getTarget(), $associated['associated']);
+ continue;
+ }
+ }
+ }
+
+ /**
+ * Checks if an association exists.
+ *
+ * @throws \RuntimeException If no such association exists for the given table.
+ * @param \Cake\ORM\Table $table Table object.
+ * @param string $association Association name.
+ * @return void
+ */
+ protected function _checkAssociation(Table $table, string $association): void
+ {
+ if (!$table->associations()->has($association)) {
+ throw new RuntimeException(sprintf(
+ 'Table `%s` is not associated with `%s`',
+ get_class($table),
+ $association
+ ));
+ }
+ }
+
+ /**
+ * Set the guard option.
+ *
+ * @param bool $guard Guard the properties or not.
+ * @return $this
+ */
+ public function guard(bool $guard)
+ {
+ $this->_options['guard'] = $guard;
+
+ return $this;
+ }
+
+ /**
+ * Set the validation rule set to use.
+ *
+ * @param string $validate Name of the validation rule set to use.
+ * @return $this
+ */
+ public function validate(string $validate)
+ {
+ $this->_table->getValidator($validate);
+ $this->_options['validate'] = $validate;
+
+ return $this;
+ }
+
+ /**
+ * Set check existing option.
+ *
+ * @param bool $checkExisting Guard the properties or not.
+ * @return $this
+ */
+ public function checkExisting(bool $checkExisting)
+ {
+ $this->_options['checkExisting'] = $checkExisting;
+
+ return $this;
+ }
+
+ /**
+ * Option to check the rules.
+ *
+ * @param bool $checkRules Check the rules or not.
+ * @return $this
+ */
+ public function checkRules(bool $checkRules)
+ {
+ $this->_options['checkRules'] = $checkRules;
+
+ return $this;
+ }
+
+ /**
+ * Sets the atomic option.
+ *
+ * @param bool $atomic Atomic or not.
+ * @return $this
+ */
+ public function atomic(bool $atomic)
+ {
+ $this->_options['atomic'] = $atomic;
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function toArray(): array
+ {
+ return $this->_options;
+ }
+
+ /**
+ * Setting custom options.
+ *
+ * @param string $option Option key.
+ * @param mixed $value Option value.
+ * @return $this
+ */
+ public function set(string $option, $value)
+ {
+ if (method_exists($this, $option)) {
+ return $this->{$option}($value);
+ }
+ $this->_options[$option] = $value;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/Table.php b/app/vendor/cakephp/cakephp/src/ORM/Table.php
new file mode 100644
index 000000000..2cb3e9583
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/Table.php
@@ -0,0 +1,3102 @@
+findByUsername('mark');
+ * ```
+ *
+ * You can also combine conditions on multiple fields using either `Or` or `And`:
+ *
+ * ```
+ * $query = $users->findByUsernameOrEmail('mark', 'mark@example.org');
+ * ```
+ *
+ * ### Bulk updates/deletes
+ *
+ * You can use Table::updateAll() and Table::deleteAll() to do bulk updates/deletes.
+ * You should be aware that events will *not* be fired for bulk updates/deletes.
+ *
+ * ### Events
+ *
+ * Table objects emit several events during as life-cycle hooks during find, delete and save
+ * operations. All events use the CakePHP event package:
+ *
+ * - `Model.beforeFind` Fired before each find operation. By stopping the event and
+ * supplying a return value you can bypass the find operation entirely. Any
+ * changes done to the $query instance will be retained for the rest of the find. The
+ * `$primary` parameter indicates whether or not this is the root query, or an
+ * associated query.
+ *
+ * - `Model.buildValidator` Allows listeners to modify validation rules
+ * for the provided named validator.
+ *
+ * - `Model.buildRules` Allows listeners to modify the rules checker by adding more rules.
+ *
+ * - `Model.beforeRules` Fired before an entity is validated using the rules checker.
+ * By stopping this event, you can return the final value of the rules checking operation.
+ *
+ * - `Model.afterRules` Fired after the rules have been checked on the entity. By
+ * stopping this event, you can return the final value of the rules checking operation.
+ *
+ * - `Model.beforeSave` Fired before each entity is saved. Stopping this event will
+ * abort the save operation. When the event is stopped the result of the event will be returned.
+ *
+ * - `Model.afterSave` Fired after an entity is saved.
+ *
+ * - `Model.afterSaveCommit` Fired after the transaction in which the save operation is
+ * wrapped has been committed. It’s also triggered for non atomic saves where database
+ * operations are implicitly committed. The event is triggered only for the primary
+ * table on which save() is directly called. The event is not triggered if a
+ * transaction is started before calling save.
+ *
+ * - `Model.beforeDelete` Fired before an entity is deleted. By stopping this
+ * event you will abort the delete operation.
+ *
+ * - `Model.afterDelete` Fired after an entity has been deleted.
+ *
+ * ### Callbacks
+ *
+ * You can subscribe to the events listed above in your table classes by implementing the
+ * lifecycle methods below:
+ *
+ * - `beforeFind(EventInterface $event, Query $query, ArrayObject $options, boolean $primary)`
+ * - `beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)`
+ * - `afterMarshal(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * - `buildValidator(EventInterface $event, Validator $validator, string $name)`
+ * - `buildRules(RulesChecker $rules)`
+ * - `beforeRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, string $operation)`
+ * - `afterRules(EventInterface $event, EntityInterface $entity, ArrayObject $options, bool $result, string $operation)`
+ * - `beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * - `afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * - `afterSaveCommit(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * - `beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * - `afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ * - `afterDeleteCommit(EventInterface $event, EntityInterface $entity, ArrayObject $options)`
+ *
+ * @see \Cake\Event\EventManager for reference on the events system.
+ * @link https://book.cakephp.org/4/en/orm/table-objects.html#event-list
+ */
+class Table implements RepositoryInterface, EventListenerInterface, EventDispatcherInterface, ValidatorAwareInterface
+{
+ use EventDispatcherTrait;
+ use RulesAwareTrait;
+ use ValidatorAwareTrait;
+
+ /**
+ * Name of default validation set.
+ *
+ * @var string
+ */
+ public const DEFAULT_VALIDATOR = 'default';
+
+ /**
+ * The alias this object is assigned to validators as.
+ *
+ * @var string
+ */
+ public const VALIDATOR_PROVIDER_NAME = 'table';
+
+ /**
+ * The name of the event dispatched when a validator has been built.
+ *
+ * @var string
+ */
+ public const BUILD_VALIDATOR_EVENT = 'Model.buildValidator';
+
+ /**
+ * The rules class name that is used.
+ *
+ * @var string
+ */
+ public const RULES_CLASS = RulesChecker::class;
+
+ /**
+ * The IsUnique class name that is used.
+ *
+ * @var string
+ */
+ public const IS_UNIQUE_CLASS = IsUnique::class;
+
+ /**
+ * Name of the table as it can be found in the database
+ *
+ * @var string|null
+ */
+ protected $_table;
+
+ /**
+ * Human name giving to this particular instance. Multiple objects representing
+ * the same database table can exist by using different aliases.
+ *
+ * @var string|null
+ */
+ protected $_alias;
+
+ /**
+ * Connection instance
+ *
+ * @var \Cake\Database\Connection|null
+ */
+ protected $_connection;
+
+ /**
+ * The schema object containing a description of this table fields
+ *
+ * @var \Cake\Database\Schema\TableSchemaInterface|null
+ */
+ protected $_schema;
+
+ /**
+ * The name of the field that represents the primary key in the table
+ *
+ * @var string|string[]|null
+ */
+ protected $_primaryKey;
+
+ /**
+ * The name of the field that represents a human readable representation of a row
+ *
+ * @var string|string[]|null
+ */
+ protected $_displayField;
+
+ /**
+ * The associations container for this Table.
+ *
+ * @var \Cake\ORM\AssociationCollection
+ */
+ protected $_associations;
+
+ /**
+ * BehaviorRegistry for this table
+ *
+ * @var \Cake\ORM\BehaviorRegistry
+ */
+ protected $_behaviors;
+
+ /**
+ * The name of the class that represent a single row for this table
+ *
+ * @var string
+ * @psalm-var class-string<\Cake\Datasource\EntityInterface>
+ */
+ protected $_entityClass;
+
+ /**
+ * Registry key used to create this table object
+ *
+ * @var string|null
+ */
+ protected $_registryAlias;
+
+ /**
+ * Initializes a new instance
+ *
+ * The $config array understands the following keys:
+ *
+ * - table: Name of the database table to represent
+ * - alias: Alias to be assigned to this table (default to table name)
+ * - connection: The connection instance to use
+ * - entityClass: The fully namespaced class name of the entity class that will
+ * represent rows in this table.
+ * - schema: A \Cake\Database\Schema\TableSchemaInterface object or an array that can be
+ * passed to it.
+ * - eventManager: An instance of an event manager to use for internal events
+ * - behaviors: A BehaviorRegistry. Generally not used outside of tests.
+ * - associations: An AssociationCollection instance.
+ * - validator: A Validator instance which is assigned as the "default"
+ * validation set, or an associative array, where key is the name of the
+ * validation set and value the Validator instance.
+ *
+ * @param array $config List of options for this table
+ */
+ public function __construct(array $config = [])
+ {
+ if (!empty($config['registryAlias'])) {
+ $this->setRegistryAlias($config['registryAlias']);
+ }
+ if (!empty($config['table'])) {
+ $this->setTable($config['table']);
+ }
+ if (!empty($config['alias'])) {
+ $this->setAlias($config['alias']);
+ }
+ if (!empty($config['connection'])) {
+ $this->setConnection($config['connection']);
+ }
+ if (!empty($config['schema'])) {
+ $this->setSchema($config['schema']);
+ }
+ if (!empty($config['entityClass'])) {
+ $this->setEntityClass($config['entityClass']);
+ }
+ $eventManager = $behaviors = $associations = null;
+ if (!empty($config['eventManager'])) {
+ $eventManager = $config['eventManager'];
+ }
+ if (!empty($config['behaviors'])) {
+ $behaviors = $config['behaviors'];
+ }
+ if (!empty($config['associations'])) {
+ $associations = $config['associations'];
+ }
+ if (!empty($config['validator'])) {
+ if (!is_array($config['validator'])) {
+ $this->setValidator(static::DEFAULT_VALIDATOR, $config['validator']);
+ } else {
+ foreach ($config['validator'] as $name => $validator) {
+ $this->setValidator($name, $validator);
+ }
+ }
+ }
+ $this->_eventManager = $eventManager ?: new EventManager();
+ $this->_behaviors = $behaviors ?: new BehaviorRegistry();
+ $this->_behaviors->setTable($this);
+ $this->_associations = $associations ?: new AssociationCollection();
+
+ $this->initialize($config);
+ $this->_eventManager->on($this);
+ $this->dispatchEvent('Model.initialize');
+ }
+
+ /**
+ * Get the default connection name.
+ *
+ * This method is used to get the fallback connection name if an
+ * instance is created through the TableLocator without a connection.
+ *
+ * @return string
+ * @see \Cake\ORM\Locator\TableLocator::get()
+ */
+ public static function defaultConnectionName(): string
+ {
+ return 'default';
+ }
+
+ /**
+ * Initialize a table instance. Called after the constructor.
+ *
+ * You can use this method to define associations, attach behaviors
+ * define validation and do any other initialization logic you need.
+ *
+ * ```
+ * public function initialize(array $config)
+ * {
+ * $this->belongsTo('Users');
+ * $this->belongsToMany('Tagging.Tags');
+ * $this->setPrimaryKey('something_else');
+ * }
+ * ```
+ *
+ * @param array $config Configuration options passed to the constructor
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ }
+
+ /**
+ * Sets the database table name.
+ *
+ * This can include the database schema name in the form 'schema.table'.
+ * If the name must be quoted, enable automatic identifier quoting.
+ *
+ * @param string $table Table name.
+ * @return $this
+ */
+ public function setTable(string $table)
+ {
+ $this->_table = $table;
+
+ return $this;
+ }
+
+ /**
+ * Returns the database table name.
+ *
+ * This can include the database schema name if set using `setTable()`.
+ *
+ * @return string
+ */
+ public function getTable(): string
+ {
+ if ($this->_table === null) {
+ $table = namespaceSplit(static::class);
+ $table = substr(end($table), 0, -5);
+ if (!$table) {
+ $table = $this->getAlias();
+ }
+ $this->_table = Inflector::underscore($table);
+ }
+
+ return $this->_table;
+ }
+
+ /**
+ * Sets the table alias.
+ *
+ * @param string $alias Table alias
+ * @return $this
+ */
+ public function setAlias(string $alias)
+ {
+ $this->_alias = $alias;
+
+ return $this;
+ }
+
+ /**
+ * Returns the table alias.
+ *
+ * @return string
+ */
+ public function getAlias(): string
+ {
+ if ($this->_alias === null) {
+ $alias = namespaceSplit(static::class);
+ $alias = substr(end($alias), 0, -5) ?: $this->getTable();
+ $this->_alias = $alias;
+ }
+
+ return $this->_alias;
+ }
+
+ /**
+ * Alias a field with the table's current alias.
+ *
+ * If field is already aliased it will result in no-op.
+ *
+ * @param string $field The field to alias.
+ * @return string The field prefixed with the table alias.
+ */
+ public function aliasField(string $field): string
+ {
+ if (strpos($field, '.') !== false) {
+ return $field;
+ }
+
+ return $this->getAlias() . '.' . $field;
+ }
+
+ /**
+ * Sets the table registry key used to create this table instance.
+ *
+ * @param string $registryAlias The key used to access this object.
+ * @return $this
+ */
+ public function setRegistryAlias(string $registryAlias)
+ {
+ $this->_registryAlias = $registryAlias;
+
+ return $this;
+ }
+
+ /**
+ * Returns the table registry key used to create this table instance.
+ *
+ * @return string
+ */
+ public function getRegistryAlias(): string
+ {
+ if ($this->_registryAlias === null) {
+ $this->_registryAlias = $this->getAlias();
+ }
+
+ return $this->_registryAlias;
+ }
+
+ /**
+ * Sets the connection instance.
+ *
+ * @param \Cake\Database\Connection $connection The connection instance
+ * @return $this
+ */
+ public function setConnection(Connection $connection)
+ {
+ $this->_connection = $connection;
+
+ return $this;
+ }
+
+ /**
+ * Returns the connection instance.
+ *
+ * @return \Cake\Database\Connection
+ */
+ public function getConnection(): Connection
+ {
+ if (!$this->_connection) {
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get(static::defaultConnectionName());
+ $this->_connection = $connection;
+ }
+
+ return $this->_connection;
+ }
+
+ /**
+ * Returns the schema table object describing this table's properties.
+ *
+ * @return \Cake\Database\Schema\TableSchemaInterface
+ */
+ public function getSchema(): TableSchemaInterface
+ {
+ if ($this->_schema === null) {
+ $this->_schema = $this->_initializeSchema(
+ $this->getConnection()
+ ->getSchemaCollection()
+ ->describe($this->getTable())
+ );
+ if (Configure::read('debug')) {
+ $this->checkAliasLengths();
+ }
+ }
+
+ return $this->_schema;
+ }
+
+ /**
+ * Sets the schema table object describing this table's properties.
+ *
+ * If an array is passed, a new TableSchemaInterface will be constructed
+ * out of it and used as the schema for this table.
+ *
+ * @param array|\Cake\Database\Schema\TableSchemaInterface $schema Schema to be used for this table
+ * @return $this
+ */
+ public function setSchema($schema)
+ {
+ if (is_array($schema)) {
+ $constraints = [];
+
+ if (isset($schema['_constraints'])) {
+ $constraints = $schema['_constraints'];
+ unset($schema['_constraints']);
+ }
+
+ $schema = $this->getConnection()->getDriver()->newTableSchema($this->getTable(), $schema);
+
+ foreach ($constraints as $name => $value) {
+ $schema->addConstraint($name, $value);
+ }
+ }
+
+ $this->_schema = $schema;
+ if (Configure::read('debug')) {
+ $this->checkAliasLengths();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Checks if all table name + column name combinations used for
+ * queries fit into the max length allowed by database driver.
+ *
+ * @return void
+ * @throws \RuntimeException When an alias combination is too long
+ */
+ protected function checkAliasLengths(): void
+ {
+ if ($this->_schema === null) {
+ throw new RuntimeException("Unable to check max alias lengths for `{$this->getAlias()}` without schema.");
+ }
+
+ $maxLength = null;
+ if (method_exists($this->getConnection()->getDriver(), 'getMaxAliasLength')) {
+ $maxLength = $this->getConnection()->getDriver()->getMaxAliasLength();
+ }
+ if ($maxLength === null) {
+ return;
+ }
+
+ $table = $this->getAlias();
+ foreach ($this->_schema->columns() as $name) {
+ if (strlen($table . '__' . $name) > $maxLength) {
+ $nameLength = $maxLength - 2;
+ throw new RuntimeException(
+ 'ORM queries generate field aliases using the table name/alias and column name. ' .
+ "The table alias `{$table}` and column `{$name}` create an alias longer than ({$nameLength}). " .
+ 'You must change the table schema in the database and shorten either the table or column ' .
+ 'identifier so they fit within the database alias limits.'
+ );
+ }
+ }
+ }
+
+ /**
+ * Override this function in order to alter the schema used by this table.
+ * This function is only called after fetching the schema out of the database.
+ * If you wish to provide your own schema to this table without touching the
+ * database, you can override schema() or inject the definitions though that
+ * method.
+ *
+ * ### Example:
+ *
+ * ```
+ * protected function _initializeSchema(\Cake\Database\Schema\TableSchemaInterface $schema) {
+ * $schema->setColumnType('preferences', 'json');
+ * return $schema;
+ * }
+ * ```
+ *
+ * @param \Cake\Database\Schema\TableSchemaInterface $schema The table definition fetched from database.
+ * @return \Cake\Database\Schema\TableSchemaInterface the altered schema
+ */
+ protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaInterface
+ {
+ return $schema;
+ }
+
+ /**
+ * Test to see if a Table has a specific field/column.
+ *
+ * Delegates to the schema object and checks for column presence
+ * using the Schema\Table instance.
+ *
+ * @param string $field The field to check for.
+ * @return bool True if the field exists, false if it does not.
+ */
+ public function hasField(string $field): bool
+ {
+ $schema = $this->getSchema();
+
+ return $schema->getColumn($field) !== null;
+ }
+
+ /**
+ * Sets the primary key field name.
+ *
+ * @param string|string[] $key Sets a new name to be used as primary key
+ * @return $this
+ */
+ public function setPrimaryKey($key)
+ {
+ $this->_primaryKey = $key;
+
+ return $this;
+ }
+
+ /**
+ * Returns the primary key field name.
+ *
+ * @return string|string[]
+ */
+ public function getPrimaryKey()
+ {
+ if ($this->_primaryKey === null) {
+ $key = $this->getSchema()->getPrimaryKey();
+ if (count($key) === 1) {
+ $key = $key[0];
+ }
+ $this->_primaryKey = $key;
+ }
+
+ return $this->_primaryKey;
+ }
+
+ /**
+ * Sets the display field.
+ *
+ * @param string|string[] $field Name to be used as display field.
+ * @return $this
+ */
+ public function setDisplayField($field)
+ {
+ $this->_displayField = $field;
+
+ return $this;
+ }
+
+ /**
+ * Returns the display field.
+ *
+ * @return string|string[]|null
+ */
+ public function getDisplayField()
+ {
+ if ($this->_displayField === null) {
+ $schema = $this->getSchema();
+ $primary = (array)$this->getPrimaryKey();
+ $this->_displayField = array_shift($primary);
+ if ($schema->getColumn('title')) {
+ $this->_displayField = 'title';
+ }
+ if ($schema->getColumn('name')) {
+ $this->_displayField = 'name';
+ }
+ }
+
+ return $this->_displayField;
+ }
+
+ /**
+ * Returns the class used to hydrate rows for this table.
+ *
+ * @return string
+ * @psalm-return class-string<\Cake\Datasource\EntityInterface>
+ */
+ public function getEntityClass(): string
+ {
+ if (!$this->_entityClass) {
+ $default = Entity::class;
+ $self = static::class;
+ $parts = explode('\\', $self);
+
+ if ($self === self::class || count($parts) < 3) {
+ return $this->_entityClass = $default;
+ }
+
+ $alias = Inflector::classify(Inflector::underscore(substr(array_pop($parts), 0, -5)));
+ $name = implode('\\', array_slice($parts, 0, -1)) . '\\Entity\\' . $alias;
+ if (!class_exists($name)) {
+ return $this->_entityClass = $default;
+ }
+
+ /** @var class-string<\Cake\Datasource\EntityInterface>|null $class */
+ $class = App::className($name, 'Model/Entity');
+ if (!$class) {
+ throw new MissingEntityException([$name]);
+ }
+
+ $this->_entityClass = $class;
+ }
+
+ return $this->_entityClass;
+ }
+
+ /**
+ * Sets the class used to hydrate rows for this table.
+ *
+ * @param string $name The name of the class to use
+ * @throws \Cake\ORM\Exception\MissingEntityException when the entity class cannot be found
+ * @return $this
+ */
+ public function setEntityClass(string $name)
+ {
+ /** @psalm-var class-string<\Cake\Datasource\EntityInterface>|null */
+ $class = App::className($name, 'Model/Entity');
+ if ($class === null) {
+ throw new MissingEntityException([$name]);
+ }
+
+ $this->_entityClass = $class;
+
+ return $this;
+ }
+
+ /**
+ * Add a behavior.
+ *
+ * Adds a behavior to this table's behavior collection. Behaviors
+ * provide an easy way to create horizontally re-usable features
+ * that can provide trait like functionality, and allow for events
+ * to be listened to.
+ *
+ * Example:
+ *
+ * Load a behavior, with some settings.
+ *
+ * ```
+ * $this->addBehavior('Tree', ['parent' => 'parentId']);
+ * ```
+ *
+ * Behaviors are generally loaded during Table::initialize().
+ *
+ * @param string $name The name of the behavior. Can be a short class reference.
+ * @param array $options The options for the behavior to use.
+ * @return $this
+ * @throws \RuntimeException If a behavior is being reloaded.
+ * @see \Cake\ORM\Behavior
+ */
+ public function addBehavior(string $name, array $options = [])
+ {
+ $this->_behaviors->load($name, $options);
+
+ return $this;
+ }
+
+ /**
+ * Adds an array of behaviors to the table's behavior collection.
+ *
+ * Example:
+ *
+ * ```
+ * $this->addBehaviors([
+ * 'Timestamp',
+ * 'Tree' => ['level' => 'level'],
+ * ]);
+ * ```
+ *
+ * @param array $behaviors All of the behaviors to load.
+ * @return $this
+ * @throws \RuntimeException If a behavior is being reloaded.
+ */
+ public function addBehaviors(array $behaviors)
+ {
+ foreach ($behaviors as $name => $options) {
+ if (is_int($name)) {
+ $name = $options;
+ $options = [];
+ }
+
+ $this->addBehavior($name, $options);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Removes a behavior from this table's behavior registry.
+ *
+ * Example:
+ *
+ * Remove a behavior from this table.
+ *
+ * ```
+ * $this->removeBehavior('Tree');
+ * ```
+ *
+ * @param string $name The alias that the behavior was added with.
+ * @return $this
+ * @see \Cake\ORM\Behavior
+ */
+ public function removeBehavior(string $name)
+ {
+ $this->_behaviors->unload($name);
+
+ return $this;
+ }
+
+ /**
+ * Returns the behavior registry for this table.
+ *
+ * @return \Cake\ORM\BehaviorRegistry The BehaviorRegistry instance.
+ */
+ public function behaviors(): BehaviorRegistry
+ {
+ return $this->_behaviors;
+ }
+
+ /**
+ * Get a behavior from the registry.
+ *
+ * @param string $name The behavior alias to get from the registry.
+ * @return \Cake\ORM\Behavior
+ * @throws \InvalidArgumentException If the behavior does not exist.
+ */
+ public function getBehavior(string $name): Behavior
+ {
+ if (!$this->_behaviors->has($name)) {
+ throw new InvalidArgumentException(sprintf(
+ 'The %s behavior is not defined on %s.',
+ $name,
+ static::class
+ ));
+ }
+
+ $behavior = $this->_behaviors->get($name);
+
+ return $behavior;
+ }
+
+ /**
+ * Check if a behavior with the given alias has been loaded.
+ *
+ * @param string $name The behavior alias to check.
+ * @return bool Whether or not the behavior exists.
+ */
+ public function hasBehavior(string $name): bool
+ {
+ return $this->_behaviors->has($name);
+ }
+
+ /**
+ * Returns an association object configured for the specified alias.
+ *
+ * The name argument also supports dot syntax to access deeper associations.
+ *
+ * ```
+ * $users = $this->getAssociation('Articles.Comments.Users');
+ * ```
+ *
+ * Note that this method requires the association to be present or otherwise
+ * throws an exception.
+ * If you are not sure, use hasAssociation() before calling this method.
+ *
+ * @param string $name The alias used for the association.
+ * @return \Cake\ORM\Association The association.
+ * @throws \InvalidArgumentException
+ */
+ public function getAssociation(string $name): Association
+ {
+ $association = $this->findAssociation($name);
+ if (!$association) {
+ $assocations = $this->associations()->keys();
+
+ $message = "The `{$name}` association is not defined on `{$this->getAlias()}`.";
+ if ($assocations) {
+ $message .= "\nValid associations are: " . implode(', ', $assocations);
+ }
+ throw new InvalidArgumentException($message);
+ }
+
+ return $association;
+ }
+
+ /**
+ * Checks whether a specific association exists on this Table instance.
+ *
+ * The name argument also supports dot syntax to access deeper associations.
+ *
+ * ```
+ * $hasUsers = $this->hasAssociation('Articles.Comments.Users');
+ * ```
+ *
+ * @param string $name The alias used for the association.
+ * @return bool
+ */
+ public function hasAssociation(string $name): bool
+ {
+ return $this->findAssociation($name) !== null;
+ }
+
+ /**
+ * Returns an association object configured for the specified alias if any.
+ *
+ * The name argument also supports dot syntax to access deeper associations.
+ *
+ * ```
+ * $users = $this->getAssociation('Articles.Comments.Users');
+ * ```
+ *
+ * @param string $name The alias used for the association.
+ * @return \Cake\ORM\Association|null Either the association or null.
+ */
+ protected function findAssociation(string $name): ?Association
+ {
+ if (strpos($name, '.') === false) {
+ return $this->_associations->get($name);
+ }
+
+ $result = null;
+ [$name, $next] = array_pad(explode('.', $name, 2), 2, null);
+ if ($name !== null) {
+ $result = $this->_associations->get($name);
+ }
+
+ if ($result !== null && $next !== null) {
+ $result = $result->getTarget()->getAssociation($next);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get the associations collection for this table.
+ *
+ * @return \Cake\ORM\AssociationCollection The collection of association objects.
+ */
+ public function associations(): AssociationCollection
+ {
+ return $this->_associations;
+ }
+
+ /**
+ * Setup multiple associations.
+ *
+ * It takes an array containing set of table names indexed by association type
+ * as argument:
+ *
+ * ```
+ * $this->Posts->addAssociations([
+ * 'belongsTo' => [
+ * 'Users' => ['className' => 'App\Model\Table\UsersTable']
+ * ],
+ * 'hasMany' => ['Comments'],
+ * 'belongsToMany' => ['Tags']
+ * ]);
+ * ```
+ *
+ * Each association type accepts multiple associations where the keys
+ * are the aliases, and the values are association config data. If numeric
+ * keys are used the values will be treated as association aliases.
+ *
+ * @param array $params Set of associations to bind (indexed by association type)
+ * @return $this
+ * @see \Cake\ORM\Table::belongsTo()
+ * @see \Cake\ORM\Table::hasOne()
+ * @see \Cake\ORM\Table::hasMany()
+ * @see \Cake\ORM\Table::belongsToMany()
+ */
+ public function addAssociations(array $params)
+ {
+ foreach ($params as $assocType => $tables) {
+ foreach ($tables as $associated => $options) {
+ if (is_numeric($associated)) {
+ $associated = $options;
+ $options = [];
+ }
+ $this->{$assocType}($associated, $options);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Creates a new BelongsTo association between this table and a target
+ * table. A "belongs to" association is a N-1 relationship where this table
+ * is the N side, and where there is a single associated record in the target
+ * table for each one in this table.
+ *
+ * Target table can be inferred by its name, which is provided in the
+ * first argument, or you can either pass the to be instantiated or
+ * an instance of it directly.
+ *
+ * The options array accept the following keys:
+ *
+ * - className: The class name of the target table object
+ * - targetTable: An instance of a table object to be used as the target table
+ * - foreignKey: The name of the field to use as foreign key, if false none
+ * will be used
+ * - conditions: array with a list of conditions to filter the join with
+ * - joinType: The type of join to be used (e.g. INNER)
+ * - strategy: The loading strategy to use. 'join' and 'select' are supported.
+ * - finder: The finder method to use when loading records from this association.
+ * Defaults to 'all'. When the strategy is 'join', only the fields, containments,
+ * and where conditions will be used from the finder.
+ *
+ * This method will return the association object that was built.
+ *
+ * @param string $associated the alias for the target table. This is used to
+ * uniquely identify the association
+ * @param array $options list of options to configure the association definition
+ * @return \Cake\ORM\Association\BelongsTo
+ */
+ public function belongsTo(string $associated, array $options = []): BelongsTo
+ {
+ $options += ['sourceTable' => $this];
+
+ /** @var \Cake\ORM\Association\BelongsTo $association */
+ $association = $this->_associations->load(BelongsTo::class, $associated, $options);
+
+ return $association;
+ }
+
+ /**
+ * Creates a new HasOne association between this table and a target
+ * table. A "has one" association is a 1-1 relationship.
+ *
+ * Target table can be inferred by its name, which is provided in the
+ * first argument, or you can either pass the class name to be instantiated or
+ * an instance of it directly.
+ *
+ * The options array accept the following keys:
+ *
+ * - className: The class name of the target table object
+ * - targetTable: An instance of a table object to be used as the target table
+ * - foreignKey: The name of the field to use as foreign key, if false none
+ * will be used
+ * - dependent: Set to true if you want CakePHP to cascade deletes to the
+ * associated table when an entity is removed on this table. The delete operation
+ * on the associated table will not cascade further. To get recursive cascades enable
+ * `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove
+ * associated data, or when you are using database constraints.
+ * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
+ * cascaded deletes. If false the ORM will use deleteAll() to remove data.
+ * When true records will be loaded and then deleted.
+ * - conditions: array with a list of conditions to filter the join with
+ * - joinType: The type of join to be used (e.g. LEFT)
+ * - strategy: The loading strategy to use. 'join' and 'select' are supported.
+ * - finder: The finder method to use when loading records from this association.
+ * Defaults to 'all'. When the strategy is 'join', only the fields, containments,
+ * and where conditions will be used from the finder.
+ *
+ * This method will return the association object that was built.
+ *
+ * @param string $associated the alias for the target table. This is used to
+ * uniquely identify the association
+ * @param array $options list of options to configure the association definition
+ * @return \Cake\ORM\Association\HasOne
+ */
+ public function hasOne(string $associated, array $options = []): HasOne
+ {
+ $options += ['sourceTable' => $this];
+
+ /** @var \Cake\ORM\Association\HasOne $association */
+ $association = $this->_associations->load(HasOne::class, $associated, $options);
+
+ return $association;
+ }
+
+ /**
+ * Creates a new HasMany association between this table and a target
+ * table. A "has many" association is a 1-N relationship.
+ *
+ * Target table can be inferred by its name, which is provided in the
+ * first argument, or you can either pass the class name to be instantiated or
+ * an instance of it directly.
+ *
+ * The options array accept the following keys:
+ *
+ * - className: The class name of the target table object
+ * - targetTable: An instance of a table object to be used as the target table
+ * - foreignKey: The name of the field to use as foreign key, if false none
+ * will be used
+ * - dependent: Set to true if you want CakePHP to cascade deletes to the
+ * associated table when an entity is removed on this table. The delete operation
+ * on the associated table will not cascade further. To get recursive cascades enable
+ * `cascadeCallbacks` as well. Set to false if you don't want CakePHP to remove
+ * associated data, or when you are using database constraints.
+ * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
+ * cascaded deletes. If false the ORM will use deleteAll() to remove data.
+ * When true records will be loaded and then deleted.
+ * - conditions: array with a list of conditions to filter the join with
+ * - sort: The order in which results for this association should be returned
+ * - saveStrategy: Either 'append' or 'replace'. When 'append' the current records
+ * are appended to any records in the database. When 'replace' associated records
+ * not in the current set will be removed. If the foreign key is a null able column
+ * or if `dependent` is true records will be orphaned.
+ * - strategy: The strategy to be used for selecting results Either 'select'
+ * or 'subquery'. If subquery is selected the query used to return results
+ * in the source table will be used as conditions for getting rows in the
+ * target table.
+ * - finder: The finder method to use when loading records from this association.
+ * Defaults to 'all'.
+ *
+ * This method will return the association object that was built.
+ *
+ * @param string $associated the alias for the target table. This is used to
+ * uniquely identify the association
+ * @param array $options list of options to configure the association definition
+ * @return \Cake\ORM\Association\HasMany
+ */
+ public function hasMany(string $associated, array $options = []): HasMany
+ {
+ $options += ['sourceTable' => $this];
+
+ /** @var \Cake\ORM\Association\HasMany $association */
+ $association = $this->_associations->load(HasMany::class, $associated, $options);
+
+ return $association;
+ }
+
+ /**
+ * Creates a new BelongsToMany association between this table and a target
+ * table. A "belongs to many" association is a M-N relationship.
+ *
+ * Target table can be inferred by its name, which is provided in the
+ * first argument, or you can either pass the class name to be instantiated or
+ * an instance of it directly.
+ *
+ * The options array accept the following keys:
+ *
+ * - className: The class name of the target table object.
+ * - targetTable: An instance of a table object to be used as the target table.
+ * - foreignKey: The name of the field to use as foreign key.
+ * - targetForeignKey: The name of the field to use as the target foreign key.
+ * - joinTable: The name of the table representing the link between the two
+ * - through: If you choose to use an already instantiated link table, set this
+ * key to a configured Table instance containing associations to both the source
+ * and target tables in this association.
+ * - dependent: Set to false, if you do not want junction table records removed
+ * when an owning record is removed.
+ * - cascadeCallbacks: Set to true if you want CakePHP to fire callbacks on
+ * cascaded deletes. If false the ORM will use deleteAll() to remove data.
+ * When true join/junction table records will be loaded and then deleted.
+ * - conditions: array with a list of conditions to filter the join with.
+ * - sort: The order in which results for this association should be returned.
+ * - strategy: The strategy to be used for selecting results Either 'select'
+ * or 'subquery'. If subquery is selected the query used to return results
+ * in the source table will be used as conditions for getting rows in the
+ * target table.
+ * - saveStrategy: Either 'append' or 'replace'. Indicates the mode to be used
+ * for saving associated entities. The former will only create new links
+ * between both side of the relation and the latter will do a wipe and
+ * replace to create the links between the passed entities when saving.
+ * - strategy: The loading strategy to use. 'select' and 'subquery' are supported.
+ * - finder: The finder method to use when loading records from this association.
+ * Defaults to 'all'.
+ *
+ * This method will return the association object that was built.
+ *
+ * @param string $associated the alias for the target table. This is used to
+ * uniquely identify the association
+ * @param array $options list of options to configure the association definition
+ * @return \Cake\ORM\Association\BelongsToMany
+ */
+ public function belongsToMany(string $associated, array $options = []): BelongsToMany
+ {
+ $options += ['sourceTable' => $this];
+
+ /** @var \Cake\ORM\Association\BelongsToMany $association */
+ $association = $this->_associations->load(BelongsToMany::class, $associated, $options);
+
+ return $association;
+ }
+
+ /**
+ * Creates a new Query for this repository and applies some defaults based on the
+ * type of search that was selected.
+ *
+ * ### Model.beforeFind event
+ *
+ * Each find() will trigger a `Model.beforeFind` event for all attached
+ * listeners. Any listener can set a valid result set using $query
+ *
+ * By default, `$options` will recognize the following keys:
+ *
+ * - fields
+ * - conditions
+ * - order
+ * - limit
+ * - offset
+ * - page
+ * - group
+ * - having
+ * - contain
+ * - join
+ *
+ * ### Usage
+ *
+ * Using the options array:
+ *
+ * ```
+ * $query = $articles->find('all', [
+ * 'conditions' => ['published' => 1],
+ * 'limit' => 10,
+ * 'contain' => ['Users', 'Comments']
+ * ]);
+ * ```
+ *
+ * Using the builder interface:
+ *
+ * ```
+ * $query = $articles->find()
+ * ->where(['published' => 1])
+ * ->limit(10)
+ * ->contain(['Users', 'Comments']);
+ * ```
+ *
+ * ### Calling finders
+ *
+ * The find() method is the entry point for custom finder methods.
+ * You can invoke a finder by specifying the type:
+ *
+ * ```
+ * $query = $articles->find('published');
+ * ```
+ *
+ * Would invoke the `findPublished` method.
+ *
+ * @param string $type the type of query to perform
+ * @param array $options An array that will be passed to Query::applyOptions()
+ * @return \Cake\ORM\Query The query builder
+ */
+ public function find(string $type = 'all', array $options = []): Query
+ {
+ $query = $this->query();
+ $query->select();
+
+ return $this->callFinder($type, $query, $options);
+ }
+
+ /**
+ * Returns the query as passed.
+ *
+ * By default findAll() applies no conditions, you
+ * can override this method in subclasses to modify how `find('all')` works.
+ *
+ * @param \Cake\ORM\Query $query The query to find with
+ * @param array $options The options to use for the find
+ * @return \Cake\ORM\Query The query builder
+ */
+ public function findAll(Query $query, array $options): Query
+ {
+ return $query;
+ }
+
+ /**
+ * Sets up a query object so results appear as an indexed array, useful for any
+ * place where you would want a list such as for populating input select boxes.
+ *
+ * When calling this finder, the fields passed are used to determine what should
+ * be used as the array key, value and optionally what to group the results by.
+ * By default the primary key for the model is used for the key, and the display
+ * field as value.
+ *
+ * The results of this finder will be in the following form:
+ *
+ * ```
+ * [
+ * 1 => 'value for id 1',
+ * 2 => 'value for id 2',
+ * 4 => 'value for id 4'
+ * ]
+ * ```
+ *
+ * You can specify which property will be used as the key and which as value
+ * by using the `$options` array, when not specified, it will use the results
+ * of calling `primaryKey` and `displayField` respectively in this table:
+ *
+ * ```
+ * $table->find('list', [
+ * 'keyField' => 'name',
+ * 'valueField' => 'age'
+ * ]);
+ * ```
+ *
+ * The `valueField` can also be an array, in which case you can also specify
+ * the `valueSeparator` option to control how the values will be concatinated:
+ *
+ * ```
+ * $table->find('list', [
+ * 'valueField' => ['first_name', 'last_name'],
+ * 'valueSeparator' => ' | ',
+ * ]);
+ *
+ *
+ * The results of this finder will be in the following form:
+ *
+ * ```
+ * [
+ * 1 => 'John | Doe',
+ * 2 => 'Steve | Smith'
+ * ]
+ * ```
+ *
+ * Results can be put together in bigger groups when they share a property, you
+ * can customize the property to use for grouping by setting `groupField`:
+ *
+ * ```
+ * $table->find('list', [
+ * 'groupField' => 'category_id',
+ * ]);
+ * ```
+ *
+ * When using a `groupField` results will be returned in this format:
+ *
+ * ```
+ * [
+ * 'group_1' => [
+ * 1 => 'value for id 1',
+ * 2 => 'value for id 2',
+ * ]
+ * 'group_2' => [
+ * 4 => 'value for id 4'
+ * ]
+ * ]
+ * ```
+ *
+ * @param \Cake\ORM\Query $query The query to find with
+ * @param array $options The options for the find
+ * @return \Cake\ORM\Query The query builder
+ */
+ public function findList(Query $query, array $options): Query
+ {
+ $options += [
+ 'keyField' => $this->getPrimaryKey(),
+ 'valueField' => $this->getDisplayField(),
+ 'groupField' => null,
+ 'valueSeparator' => ';',
+ ];
+
+ if (
+ !$query->clause('select') &&
+ !is_object($options['keyField']) &&
+ !is_object($options['valueField']) &&
+ !is_object($options['groupField'])
+ ) {
+ $fields = array_merge(
+ (array)$options['keyField'],
+ (array)$options['valueField'],
+ (array)$options['groupField']
+ );
+ $columns = $this->getSchema()->columns();
+ if (count($fields) === count(array_intersect($fields, $columns))) {
+ $query->select($fields);
+ }
+ }
+
+ $options = $this->_setFieldMatchers(
+ $options,
+ ['keyField', 'valueField', 'groupField']
+ );
+
+ return $query->formatResults(function ($results) use ($options) {
+ /** @var \Cake\Collection\CollectionInterface $results */
+ return $results->combine(
+ $options['keyField'],
+ $options['valueField'],
+ $options['groupField']
+ );
+ });
+ }
+
+ /**
+ * Results for this finder will be a nested array, and is appropriate if you want
+ * to use the parent_id field of your model data to build nested results.
+ *
+ * Values belonging to a parent row based on their parent_id value will be
+ * recursively nested inside the parent row values using the `children` property
+ *
+ * You can customize what fields are used for nesting results, by default the
+ * primary key and the `parent_id` fields are used. If you wish to change
+ * these defaults you need to provide the keys `keyField`, `parentField` or `nestingKey` in
+ * `$options`:
+ *
+ * ```
+ * $table->find('threaded', [
+ * 'keyField' => 'id',
+ * 'parentField' => 'ancestor_id'
+ * 'nestingKey' => 'children'
+ * ]);
+ * ```
+ *
+ * @param \Cake\ORM\Query $query The query to find with
+ * @param array $options The options to find with
+ * @return \Cake\ORM\Query The query builder
+ */
+ public function findThreaded(Query $query, array $options): Query
+ {
+ $options += [
+ 'keyField' => $this->getPrimaryKey(),
+ 'parentField' => 'parent_id',
+ 'nestingKey' => 'children',
+ ];
+
+ $options = $this->_setFieldMatchers($options, ['keyField', 'parentField']);
+
+ return $query->formatResults(function ($results) use ($options) {
+ /** @var \Cake\Collection\CollectionInterface $results */
+ return $results->nest($options['keyField'], $options['parentField'], $options['nestingKey']);
+ });
+ }
+
+ /**
+ * Out of an options array, check if the keys described in `$keys` are arrays
+ * and change the values for closures that will concatenate the each of the
+ * properties in the value array when passed a row.
+ *
+ * This is an auxiliary function used for result formatters that can accept
+ * composite keys when comparing values.
+ *
+ * @param array $options the original options passed to a finder
+ * @param string[] $keys the keys to check in $options to build matchers from
+ * the associated value
+ * @return array
+ */
+ protected function _setFieldMatchers(array $options, array $keys): array
+ {
+ foreach ($keys as $field) {
+ if (!is_array($options[$field])) {
+ continue;
+ }
+
+ if (count($options[$field]) === 1) {
+ $options[$field] = current($options[$field]);
+ continue;
+ }
+
+ $fields = $options[$field];
+ $glue = in_array($field, ['keyField', 'parentField'], true) ? ';' : $options['valueSeparator'];
+ $options[$field] = function ($row) use ($fields, $glue) {
+ $matches = [];
+ foreach ($fields as $field) {
+ $matches[] = $row[$field];
+ }
+
+ return implode($glue, $matches);
+ };
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * ### Usage
+ *
+ * Get an article and some relationships:
+ *
+ * ```
+ * $article = $articles->get(1, ['contain' => ['Users', 'Comments']]);
+ * ```
+ *
+ * @param mixed $primaryKey primary key value to find
+ * @param array $options options accepted by `Table::find()`
+ * @return \Cake\Datasource\EntityInterface
+ * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id
+ * could not be found
+ * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException When $primaryKey has an
+ * incorrect number of elements.
+ * @see \Cake\Datasource\RepositoryInterface::find()
+ * @psalm-suppress InvalidReturnType
+ */
+ public function get($primaryKey, array $options = []): EntityInterface
+ {
+ $key = (array)$this->getPrimaryKey();
+ $alias = $this->getAlias();
+ foreach ($key as $index => $keyname) {
+ $key[$index] = $alias . '.' . $keyname;
+ }
+ $primaryKey = (array)$primaryKey;
+ if (count($key) !== count($primaryKey)) {
+ $primaryKey = $primaryKey ?: [null];
+ $primaryKey = array_map(function ($key) {
+ return var_export($key, true);
+ }, $primaryKey);
+
+ throw new InvalidPrimaryKeyException(sprintf(
+ 'Record not found in table "%s" with primary key [%s]',
+ $this->getTable(),
+ implode(', ', $primaryKey)
+ ));
+ }
+ $conditions = array_combine($key, $primaryKey);
+
+ $cacheConfig = $options['cache'] ?? false;
+ $cacheKey = $options['key'] ?? false;
+ $finder = $options['finder'] ?? 'all';
+ unset($options['key'], $options['cache'], $options['finder']);
+
+ $query = $this->find($finder, $options)->where($conditions);
+
+ if ($cacheConfig) {
+ if (!$cacheKey) {
+ $cacheKey = sprintf(
+ 'get:%s.%s%s',
+ $this->getConnection()->configName(),
+ $this->getTable(),
+ json_encode($primaryKey)
+ );
+ }
+ $query->cache($cacheKey, $cacheConfig);
+ }
+
+ /** @psalm-suppress InvalidReturnStatement */
+ return $query->firstOrFail();
+ }
+
+ /**
+ * Handles the logic executing of a worker inside a transaction.
+ *
+ * @param callable $worker The worker that will run inside the transaction.
+ * @param bool $atomic Whether to execute the worker inside a database transaction.
+ * @return mixed
+ */
+ protected function _executeTransaction(callable $worker, bool $atomic = true)
+ {
+ if ($atomic) {
+ return $this->getConnection()->transactional(function () use ($worker) {
+ return $worker();
+ });
+ }
+
+ return $worker();
+ }
+
+ /**
+ * Checks if the caller would have executed a commit on a transaction.
+ *
+ * @param bool $atomic True if an atomic transaction was used.
+ * @param bool $primary True if a primary was used.
+ * @return bool Returns true if a transaction was committed.
+ */
+ protected function _transactionCommitted(bool $atomic, bool $primary): bool
+ {
+ return !$this->getConnection()->inTransaction() && ($atomic || $primary);
+ }
+
+ /**
+ * Finds an existing record or creates a new one.
+ *
+ * A find() will be done to locate an existing record using the attributes
+ * defined in $search. If records matches the conditions, the first record
+ * will be returned.
+ *
+ * If no record can be found, a new entity will be created
+ * with the $search properties. If a callback is provided, it will be
+ * called allowing you to define additional default values. The new
+ * entity will be saved and returned.
+ *
+ * If your find conditions require custom order, associations or conditions, then the $search
+ * parameter can be a callable that takes the Query as the argument, or a \Cake\ORM\Query object passed
+ * as the $search parameter. Allowing you to customize the find results.
+ *
+ * ### Options
+ *
+ * The options array is passed to the save method with exception to the following keys:
+ *
+ * - atomic: Whether to execute the methods for find, save and callbacks inside a database
+ * transaction (default: true)
+ * - defaults: Whether to use the search criteria as default values for the new entity (default: true)
+ *
+ * @param array|callable|\Cake\ORM\Query $search The criteria to find existing
+ * records by. Note that when you pass a query object you'll have to use
+ * the 2nd arg of the method to modify the entity data before saving.
+ * @param callable|null $callback A callback that will be invoked for newly
+ * created entities. This callback will be called *before* the entity
+ * is persisted.
+ * @param array $options The options to use when saving.
+ * @return \Cake\Datasource\EntityInterface An entity.
+ * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
+ */
+ public function findOrCreate($search, ?callable $callback = null, $options = []): EntityInterface
+ {
+ $options = new ArrayObject($options + [
+ 'atomic' => true,
+ 'defaults' => true,
+ ]);
+
+ $entity = $this->_executeTransaction(function () use ($search, $callback, $options) {
+ return $this->_processFindOrCreate($search, $callback, $options->getArrayCopy());
+ }, $options['atomic']);
+
+ if ($entity && $this->_transactionCommitted($options['atomic'], true)) {
+ $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Performs the actual find and/or create of an entity based on the passed options.
+ *
+ * @param array|callable|\Cake\ORM\Query $search The criteria to find an existing record by, or a callable tha will
+ * customize the find query.
+ * @param callable|null $callback A callback that will be invoked for newly
+ * created entities. This callback will be called *before* the entity
+ * is persisted.
+ * @param array $options The options to use when saving.
+ * @return \Cake\Datasource\EntityInterface|array An entity.
+ * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
+ * @throws \InvalidArgumentException
+ */
+ protected function _processFindOrCreate($search, ?callable $callback = null, $options = [])
+ {
+ $query = $this->_getFindOrCreateQuery($search);
+
+ $row = $query->first();
+ if ($row !== null) {
+ return $row;
+ }
+
+ $entity = $this->newEmptyEntity();
+ if ($options['defaults'] && is_array($search)) {
+ $accessibleFields = array_combine(array_keys($search), array_fill(0, count($search), true));
+ $entity = $this->patchEntity($entity, $search, ['accessibleFields' => $accessibleFields]);
+ }
+ if ($callback !== null) {
+ $entity = $callback($entity) ?: $entity;
+ }
+ unset($options['defaults']);
+
+ $result = $this->save($entity, $options);
+
+ if ($result === false) {
+ throw new PersistenceFailedException($entity, ['findOrCreate']);
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Gets the query object for findOrCreate().
+ *
+ * @param array|callable|\Cake\ORM\Query $search The criteria to find existing records by.
+ * @return \Cake\ORM\Query
+ */
+ protected function _getFindOrCreateQuery($search): Query
+ {
+ if (is_callable($search)) {
+ $query = $this->find();
+ $search($query);
+ } elseif (is_array($search)) {
+ $query = $this->find()->where($search);
+ } elseif ($search instanceof Query) {
+ $query = $search;
+ } else {
+ throw new InvalidArgumentException(sprintf(
+ 'Search criteria must be an array, callable or Query. Got "%s"',
+ getTypeName($search)
+ ));
+ }
+
+ return $query;
+ }
+
+ /**
+ * Creates a new Query instance for a table.
+ *
+ * @return \Cake\ORM\Query
+ */
+ public function query(): Query
+ {
+ return new Query($this->getConnection(), $this);
+ }
+
+ /**
+ * Creates a new Query::subquery() instance for a table.
+ *
+ * @return \Cake\ORM\Query
+ * @see \Cake\ORM\Query::subquery()
+ */
+ public function subquery(): Query
+ {
+ return Query::subquery($this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function updateAll($fields, $conditions): int
+ {
+ $query = $this->query();
+ $query->update()
+ ->set($fields)
+ ->where($conditions);
+ $statement = $query->execute();
+ $statement->closeCursor();
+
+ return $statement->rowCount();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function deleteAll($conditions): int
+ {
+ $query = $this->query()
+ ->delete()
+ ->where($conditions);
+ $statement = $query->execute();
+ $statement->closeCursor();
+
+ return $statement->rowCount();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function exists($conditions): bool
+ {
+ return (bool)count(
+ $this->find('all')
+ ->select(['existing' => 1])
+ ->where($conditions)
+ ->limit(1)
+ ->disableHydration()
+ ->toArray()
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * ### Options
+ *
+ * The options array accepts the following keys:
+ *
+ * - atomic: Whether to execute the save and callbacks inside a database
+ * transaction (default: true)
+ * - checkRules: Whether or not to check the rules on entity before saving, if the checking
+ * fails, it will abort the save operation. (default:true)
+ * - associated: If `true` it will save 1st level associated entities as they are found
+ * in the passed `$entity` whenever the property defined for the association
+ * is marked as dirty. If an array, it will be interpreted as the list of associations
+ * to be saved. It is possible to provide different options for saving on associated
+ * table objects using this key by making the custom options the array value.
+ * If `false` no associated records will be saved. (default: `true`)
+ * - checkExisting: Whether or not to check if the entity already exists, assuming that the
+ * entity is marked as not new, and the primary key has been set.
+ *
+ * ### Events
+ *
+ * When saving, this method will trigger four events:
+ *
+ * - Model.beforeRules: Will be triggered right before any rule checking is done
+ * for the passed entity if the `checkRules` key in $options is not set to false.
+ * Listeners will receive as arguments the entity, options array and the operation type.
+ * If the event is stopped the rules check result will be set to the result of the event itself.
+ * - Model.afterRules: Will be triggered right after the `checkRules()` method is
+ * called for the entity. Listeners will receive as arguments the entity,
+ * options array, the result of checking the rules and the operation type.
+ * If the event is stopped the checking result will be set to the result of
+ * the event itself.
+ * - Model.beforeSave: Will be triggered just before the list of fields to be
+ * persisted is calculated. It receives both the entity and the options as
+ * arguments. The options array is passed as an ArrayObject, so any changes in
+ * it will be reflected in every listener and remembered at the end of the event
+ * so it can be used for the rest of the save operation. Returning false in any
+ * of the listeners will abort the saving process. If the event is stopped
+ * using the event API, the event object's `result` property will be returned.
+ * This can be useful when having your own saving strategy implemented inside a
+ * listener.
+ * - Model.afterSave: Will be triggered after a successful insert or save,
+ * listeners will receive the entity and the options array as arguments. The type
+ * of operation performed (insert or update) can be determined by checking the
+ * entity's method `isNew`, true meaning an insert and false an update.
+ * - Model.afterSaveCommit: Will be triggered after the transaction is committed
+ * for atomic save, listeners will receive the entity and the options array
+ * as arguments.
+ *
+ * This method will determine whether the passed entity needs to be
+ * inserted or updated in the database. It does that by checking the `isNew`
+ * method on the entity. If the entity to be saved returns a non-empty value from
+ * its `errors()` method, it will not be saved.
+ *
+ * ### Saving on associated tables
+ *
+ * This method will by default persist entities belonging to associated tables,
+ * whenever a dirty property matching the name of the property name set for an
+ * association in this table. It is possible to control what associations will
+ * be saved and to pass additional option for saving them.
+ *
+ * ```
+ * // Only save the comments association
+ * $articles->save($entity, ['associated' => ['Comments']]);
+ *
+ * // Save the company, the employees and related addresses for each of them.
+ * // For employees do not check the entity rules
+ * $companies->save($entity, [
+ * 'associated' => [
+ * 'Employees' => [
+ * 'associated' => ['Addresses'],
+ * 'checkRules' => false
+ * ]
+ * ]
+ * ]);
+ *
+ * // Save no associations
+ * $articles->save($entity, ['associated' => false]);
+ * ```
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
+ * @param array|\ArrayAccess|\Cake\ORM\SaveOptionsBuilder $options The options to use when saving.
+ * @return \Cake\Datasource\EntityInterface|false
+ * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction is aborted in the afterSave event.
+ */
+ public function save(EntityInterface $entity, $options = [])
+ {
+ if ($options instanceof SaveOptionsBuilder) {
+ $options = $options->toArray();
+ }
+
+ $options = new ArrayObject((array)$options + [
+ 'atomic' => true,
+ 'associated' => true,
+ 'checkRules' => true,
+ 'checkExisting' => true,
+ '_primary' => true,
+ ]);
+
+ if ($entity->hasErrors((bool)$options['associated'])) {
+ return false;
+ }
+
+ if ($entity->isNew() === false && !$entity->isDirty()) {
+ return $entity;
+ }
+
+ $success = $this->_executeTransaction(function () use ($entity, $options) {
+ return $this->_processSave($entity, $options);
+ }, $options['atomic']);
+
+ if ($success) {
+ if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) {
+ $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
+ }
+ if ($options['atomic'] || $options['_primary']) {
+ $entity->clean();
+ $entity->setNew(false);
+ $entity->setSource($this->getRegistryAlias());
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Try to save an entity or throw a PersistenceFailedException if the application rules checks failed,
+ * the entity contains errors or the save was aborted by a callback.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
+ * @param array|\ArrayAccess $options The options to use when saving.
+ * @return \Cake\Datasource\EntityInterface
+ * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved
+ * @see \Cake\ORM\Table::save()
+ */
+ public function saveOrFail(EntityInterface $entity, $options = []): EntityInterface
+ {
+ $saved = $this->save($entity, $options);
+ if ($saved === false) {
+ throw new PersistenceFailedException($entity, ['save']);
+ }
+
+ return $saved;
+ }
+
+ /**
+ * Performs the actual saving of an entity based on the passed options.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
+ * @param \ArrayObject $options the options to use for the save operation
+ * @return \Cake\Datasource\EntityInterface|false
+ * @throws \RuntimeException When an entity is missing some of the primary keys.
+ * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction
+ * is aborted in the afterSave event.
+ */
+ protected function _processSave(EntityInterface $entity, ArrayObject $options)
+ {
+ $primaryColumns = (array)$this->getPrimaryKey();
+
+ if ($options['checkExisting'] && $primaryColumns && $entity->isNew() && $entity->has($primaryColumns)) {
+ $alias = $this->getAlias();
+ $conditions = [];
+ foreach ($entity->extract($primaryColumns) as $k => $v) {
+ $conditions["$alias.$k"] = $v;
+ }
+ $entity->setNew(!$this->exists($conditions));
+ }
+
+ $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE;
+ if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) {
+ return false;
+ }
+
+ $options['associated'] = $this->_associations->normalizeKeys($options['associated']);
+ $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options'));
+
+ if ($event->isStopped()) {
+ $result = $event->getResult();
+ if ($result === null) {
+ return false;
+ }
+
+ if ($result !== false && !($result instanceof EntityInterface)) {
+ throw new RuntimeException(sprintf(
+ 'The beforeSave callback must return `false` or `EntityInterface` instance. Got `%s` instead.',
+ getTypeName($result)
+ ));
+ }
+
+ return $result;
+ }
+
+ $saved = $this->_associations->saveParents(
+ $this,
+ $entity,
+ $options['associated'],
+ ['_primary' => false] + $options->getArrayCopy()
+ );
+
+ if (!$saved && $options['atomic']) {
+ return false;
+ }
+
+ $data = $entity->extract($this->getSchema()->columns(), true);
+ $isNew = $entity->isNew();
+
+ if ($isNew) {
+ $success = $this->_insert($entity, $data);
+ } else {
+ $success = $this->_update($entity, $data);
+ }
+
+ if ($success) {
+ $success = $this->_onSaveSuccess($entity, $options);
+ }
+
+ if (!$success && $isNew) {
+ $entity->unset($this->getPrimaryKey());
+ $entity->setNew(true);
+ }
+
+ return $success ? $entity : false;
+ }
+
+ /**
+ * Handles the saving of children associations and executing the afterSave logic
+ * once the entity for this table has been saved successfully.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity to be saved
+ * @param \ArrayObject $options the options to use for the save operation
+ * @return bool True on success
+ * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction
+ * is aborted in the afterSave event.
+ */
+ protected function _onSaveSuccess(EntityInterface $entity, ArrayObject $options): bool
+ {
+ $success = $this->_associations->saveChildren(
+ $this,
+ $entity,
+ $options['associated'],
+ ['_primary' => false] + $options->getArrayCopy()
+ );
+
+ if (!$success && $options['atomic']) {
+ return false;
+ }
+
+ $this->dispatchEvent('Model.afterSave', compact('entity', 'options'));
+
+ if ($options['atomic'] && !$this->getConnection()->inTransaction()) {
+ throw new RolledbackTransactionException(['table' => static::class]);
+ }
+
+ if (!$options['atomic'] && !$options['_primary']) {
+ $entity->clean();
+ $entity->setNew(false);
+ $entity->setSource($this->getRegistryAlias());
+ }
+
+ return true;
+ }
+
+ /**
+ * Auxiliary function to handle the insert of an entity's data in the table
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted
+ * @param array $data The actual data that needs to be saved
+ * @return \Cake\Datasource\EntityInterface|false
+ * @throws \RuntimeException if not all the primary keys where supplied or could
+ * be generated when the table has composite primary keys. Or when the table has no primary key.
+ */
+ protected function _insert(EntityInterface $entity, array $data)
+ {
+ $primary = (array)$this->getPrimaryKey();
+ if (empty($primary)) {
+ $msg = sprintf(
+ 'Cannot insert row in "%s" table, it has no primary key.',
+ $this->getTable()
+ );
+ throw new RuntimeException($msg);
+ }
+ $keys = array_fill(0, count($primary), null);
+ $id = (array)$this->_newId($primary) + $keys;
+
+ // Generate primary keys preferring values in $data.
+ $primary = array_combine($primary, $id) ?: [];
+ $primary = array_intersect_key($data, $primary) + $primary;
+
+ $filteredKeys = array_filter($primary, function ($v) {
+ return $v !== null;
+ });
+ $data += $filteredKeys;
+
+ if (count($primary) > 1) {
+ $schema = $this->getSchema();
+ foreach ($primary as $k => $v) {
+ if (!isset($data[$k]) && empty($schema->getColumn($k)['autoIncrement'])) {
+ $msg = 'Cannot insert row, some of the primary key values are missing. ';
+ $msg .= sprintf(
+ 'Got (%s), expecting (%s)',
+ implode(', ', $filteredKeys + $entity->extract(array_keys($primary))),
+ implode(', ', array_keys($primary))
+ );
+ throw new RuntimeException($msg);
+ }
+ }
+ }
+
+ $success = false;
+ if (empty($data)) {
+ return $success;
+ }
+
+ $statement = $this->query()->insert(array_keys($data))
+ ->values($data)
+ ->execute();
+
+ if ($statement->rowCount() !== 0) {
+ $success = $entity;
+ $entity->set($filteredKeys, ['guard' => false]);
+ $schema = $this->getSchema();
+ $driver = $this->getConnection()->getDriver();
+ foreach ($primary as $key => $v) {
+ if (!isset($data[$key])) {
+ $id = $statement->lastInsertId($this->getTable(), $key);
+ /** @var string $type */
+ $type = $schema->getColumnType($key);
+ $entity->set($key, TypeFactory::build($type)->toPHP($id, $driver));
+ break;
+ }
+ }
+ }
+ $statement->closeCursor();
+
+ return $success;
+ }
+
+ /**
+ * Generate a primary key value for a new record.
+ *
+ * By default, this uses the type system to generate a new primary key
+ * value if possible. You can override this method if you have specific requirements
+ * for id generation.
+ *
+ * Note: The ORM will not generate primary key values for composite primary keys.
+ * You can overwrite _newId() in your table class.
+ *
+ * @param string[] $primary The primary key columns to get a new ID for.
+ * @return string|null Either null or the primary key value or a list of primary key values.
+ */
+ protected function _newId(array $primary)
+ {
+ if (!$primary || count($primary) > 1) {
+ return null;
+ }
+ /** @var string $typeName */
+ $typeName = $this->getSchema()->getColumnType($primary[0]);
+ $type = TypeFactory::build($typeName);
+
+ return $type->newId();
+ }
+
+ /**
+ * Auxiliary function to handle the update of an entity's data in the table
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted
+ * @param array $data The actual data that needs to be saved
+ * @return \Cake\Datasource\EntityInterface|false
+ * @throws \InvalidArgumentException When primary key data is missing.
+ */
+ protected function _update(EntityInterface $entity, array $data)
+ {
+ $primaryColumns = (array)$this->getPrimaryKey();
+ $primaryKey = $entity->extract($primaryColumns);
+
+ $data = array_diff_key($data, $primaryKey);
+ if (empty($data)) {
+ return $entity;
+ }
+
+ if (count($primaryColumns) === 0) {
+ $entityClass = get_class($entity);
+ $table = $this->getTable();
+ $message = "Cannot update `$entityClass`. The `$table` has no primary key.";
+ throw new InvalidArgumentException($message);
+ }
+
+ if (!$entity->has($primaryColumns)) {
+ $message = 'All primary key value(s) are needed for updating, ';
+ $message .= get_class($entity) . ' is missing ' . implode(', ', $primaryColumns);
+ throw new InvalidArgumentException($message);
+ }
+
+ $query = $this->query();
+ $statement = $query->update()
+ ->set($data)
+ ->where($primaryKey)
+ ->execute();
+
+ $success = false;
+ if ($statement->errorCode() === '00000') {
+ $success = $entity;
+ }
+ $statement->closeCursor();
+
+ return $success;
+ }
+
+ /**
+ * Persists multiple entities of a table.
+ *
+ * The records will be saved in a transaction which will be rolled back if
+ * any one of the records fails to save due to failed validation or database
+ * error.
+ *
+ * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to save.
+ * @param array|\ArrayAccess|\Cake\ORM\SaveOptionsBuilder $options Options used when calling Table::save() for each entity.
+ * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface|false False on failure, entities list on success.
+ * @throws \Exception
+ */
+ public function saveMany(iterable $entities, $options = [])
+ {
+ try {
+ return $this->_saveMany($entities, $options);
+ } catch (PersistenceFailedException $exception) {
+ return false;
+ }
+ }
+
+ /**
+ * Persists multiple entities of a table.
+ *
+ * The records will be saved in a transaction which will be rolled back if
+ * any one of the records fails to save due to failed validation or database
+ * error.
+ *
+ * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to save.
+ * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity.
+ * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface Entities list.
+ * @throws \Exception
+ * @throws \Cake\ORM\Exception\PersistenceFailedException If an entity couldn't be saved.
+ */
+ public function saveManyOrFail(iterable $entities, $options = []): iterable
+ {
+ return $this->_saveMany($entities, $options);
+ }
+
+ /**
+ * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to save.
+ * @param array|\ArrayAccess|\Cake\ORM\SaveOptionsBuilder $options Options used when calling Table::save() for each entity.
+ * @throws \Cake\ORM\Exception\PersistenceFailedException If an entity couldn't be saved.
+ * @throws \Exception If an entity couldn't be saved.
+ * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface Entities list.
+ */
+ protected function _saveMany(iterable $entities, $options = []): iterable
+ {
+ $options = new ArrayObject(
+ (array)$options + [
+ 'atomic' => true,
+ 'checkRules' => true,
+ '_primary' => true,
+ ]
+ );
+
+ /** @var bool[] $isNew */
+ $isNew = [];
+ $cleanup = function ($entities) use (&$isNew): void {
+ /** @var array<\Cake\Datasource\EntityInterface> $entities */
+ foreach ($entities as $key => $entity) {
+ if (isset($isNew[$key]) && $isNew[$key]) {
+ $entity->unset($this->getPrimaryKey());
+ $entity->setNew(true);
+ }
+ }
+ };
+
+ /** @var \Cake\Datasource\EntityInterface|null $failed */
+ $failed = null;
+ try {
+ $this->getConnection()
+ ->transactional(function () use ($entities, $options, &$isNew, &$failed) {
+ foreach ($entities as $key => $entity) {
+ $isNew[$key] = $entity->isNew();
+ if ($this->save($entity, $options) === false) {
+ $failed = $entity;
+
+ return false;
+ }
+ }
+ });
+ } catch (Exception $e) {
+ $cleanup($entities);
+
+ throw $e;
+ }
+
+ if ($failed !== null) {
+ $cleanup($entities);
+
+ throw new PersistenceFailedException($failed, ['saveMany']);
+ }
+
+ if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) {
+ foreach ($entities as $entity) {
+ $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options'));
+ }
+ }
+
+ return $entities;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * For HasMany and HasOne associations records will be removed based on
+ * the dependent option. Join table records in BelongsToMany associations
+ * will always be removed. You can use the `cascadeCallbacks` option
+ * when defining associations to change how associated data is deleted.
+ *
+ * ### Options
+ *
+ * - `atomic` Defaults to true. When true the deletion happens within a transaction.
+ * - `checkRules` Defaults to true. Check deletion rules before deleting the record.
+ *
+ * ### Events
+ *
+ * - `Model.beforeDelete` Fired before the delete occurs. If stopped the delete
+ * will be aborted. Receives the event, entity, and options.
+ * - `Model.afterDelete` Fired after the delete has been successful. Receives
+ * the event, entity, and options.
+ * - `Model.afterDeleteCommit` Fired after the transaction is committed for
+ * an atomic delete. Receives the event, entity, and options.
+ *
+ * The options argument will be converted into an \ArrayObject instance
+ * for the duration of the callbacks, this allows listeners to modify
+ * the options used in the delete operation.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to remove.
+ * @param array|\ArrayAccess $options The options for the delete.
+ * @return bool success
+ */
+ public function delete(EntityInterface $entity, $options = []): bool
+ {
+ $options = new ArrayObject((array)$options + [
+ 'atomic' => true,
+ 'checkRules' => true,
+ '_primary' => true,
+ ]);
+
+ $success = $this->_executeTransaction(function () use ($entity, $options) {
+ return $this->_processDelete($entity, $options);
+ }, $options['atomic']);
+
+ if ($success && $this->_transactionCommitted($options['atomic'], $options['_primary'])) {
+ $this->dispatchEvent('Model.afterDeleteCommit', [
+ 'entity' => $entity,
+ 'options' => $options,
+ ]);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Deletes multiple entities of a table.
+ *
+ * The records will be deleted in a transaction which will be rolled back if
+ * any one of the records fails to delete due to failed validation or database
+ * error.
+ *
+ * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to delete.
+ * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity.
+ * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface|false Entities list
+ * on success, false on failure.
+ * @see \Cake\ORM\Table::delete() for options and events related to this method.
+ */
+ public function deleteMany(iterable $entities, $options = [])
+ {
+ $failed = $this->_deleteMany($entities, $options);
+
+ if ($failed !== null) {
+ return false;
+ }
+
+ return $entities;
+ }
+
+ /**
+ * Deletes multiple entities of a table.
+ *
+ * The records will be deleted in a transaction which will be rolled back if
+ * any one of the records fails to delete due to failed validation or database
+ * error.
+ *
+ * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to delete.
+ * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity.
+ * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface Entities list.
+ * @throws \Cake\ORM\Exception\PersistenceFailedException
+ * @see \Cake\ORM\Table::delete() for options and events related to this method.
+ */
+ public function deleteManyOrFail(iterable $entities, $options = []): iterable
+ {
+ $failed = $this->_deleteMany($entities, $options);
+
+ if ($failed !== null) {
+ throw new PersistenceFailedException($failed, ['deleteMany']);
+ }
+
+ return $entities;
+ }
+
+ /**
+ * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to delete.
+ * @param array|\ArrayAccess $options Options used.
+ * @return \Cake\Datasource\EntityInterface|null
+ */
+ protected function _deleteMany(iterable $entities, $options = []): ?EntityInterface
+ {
+ $options = new ArrayObject((array)$options + [
+ 'atomic' => true,
+ 'checkRules' => true,
+ '_primary' => true,
+ ]);
+
+ $failed = $this->_executeTransaction(function () use ($entities, $options) {
+ foreach ($entities as $entity) {
+ if (!$this->_processDelete($entity, $options)) {
+ return $entity;
+ }
+ }
+
+ return null;
+ }, $options['atomic']);
+
+ if ($failed === null && $this->_transactionCommitted($options['atomic'], $options['_primary'])) {
+ foreach ($entities as $entity) {
+ $this->dispatchEvent('Model.afterDeleteCommit', [
+ 'entity' => $entity,
+ 'options' => $options,
+ ]);
+ }
+ }
+
+ return $failed;
+ }
+
+ /**
+ * Try to delete an entity or throw a PersistenceFailedException if the entity is new,
+ * has no primary key value, application rules checks failed or the delete was aborted by a callback.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to remove.
+ * @param array|\ArrayAccess $options The options for the delete.
+ * @return true
+ * @throws \Cake\ORM\Exception\PersistenceFailedException
+ * @see \Cake\ORM\Table::delete()
+ */
+ public function deleteOrFail(EntityInterface $entity, $options = []): bool
+ {
+ $deleted = $this->delete($entity, $options);
+ if ($deleted === false) {
+ throw new PersistenceFailedException($entity, ['delete']);
+ }
+
+ return $deleted;
+ }
+
+ /**
+ * Perform the delete operation.
+ *
+ * Will delete the entity provided. Will remove rows from any
+ * dependent associations, and clear out join tables for BelongsToMany associations.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity The entity to delete.
+ * @param \ArrayObject $options The options for the delete.
+ * @throws \InvalidArgumentException if there are no primary key values of the
+ * passed entity
+ * @return bool success
+ */
+ protected function _processDelete(EntityInterface $entity, ArrayObject $options): bool
+ {
+ if ($entity->isNew()) {
+ return false;
+ }
+
+ $primaryKey = (array)$this->getPrimaryKey();
+ if (!$entity->has($primaryKey)) {
+ $msg = 'Deleting requires all primary key values.';
+ throw new InvalidArgumentException($msg);
+ }
+
+ if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE, $options)) {
+ return false;
+ }
+
+ $event = $this->dispatchEvent('Model.beforeDelete', [
+ 'entity' => $entity,
+ 'options' => $options,
+ ]);
+
+ if ($event->isStopped()) {
+ return (bool)$event->getResult();
+ }
+
+ $success = $this->_associations->cascadeDelete(
+ $entity,
+ ['_primary' => false] + $options->getArrayCopy()
+ );
+ if (!$success) {
+ return $success;
+ }
+
+ $query = $this->query();
+ $conditions = $entity->extract($primaryKey);
+ $statement = $query->delete()
+ ->where($conditions)
+ ->execute();
+
+ $success = $statement->rowCount() > 0;
+ if (!$success) {
+ return $success;
+ }
+
+ $this->dispatchEvent('Model.afterDelete', [
+ 'entity' => $entity,
+ 'options' => $options,
+ ]);
+
+ return $success;
+ }
+
+ /**
+ * Returns true if the finder exists for the table
+ *
+ * @param string $type name of finder to check
+ * @return bool
+ */
+ public function hasFinder(string $type): bool
+ {
+ $finder = 'find' . $type;
+
+ return method_exists($this, $finder) || $this->_behaviors->hasFinder($type);
+ }
+
+ /**
+ * Calls a finder method directly and applies it to the passed query,
+ * if no query is passed a new one will be created and returned
+ *
+ * @param string $type name of the finder to be called
+ * @param \Cake\ORM\Query $query The query object to apply the finder options to
+ * @param array $options List of options to pass to the finder
+ * @return \Cake\ORM\Query
+ * @throws \BadMethodCallException
+ */
+ public function callFinder(string $type, Query $query, array $options = []): Query
+ {
+ $query->applyOptions($options);
+ $options = $query->getOptions();
+ $finder = 'find' . $type;
+ if (method_exists($this, $finder)) {
+ return $this->{$finder}($query, $options);
+ }
+
+ if ($this->_behaviors->hasFinder($type)) {
+ return $this->_behaviors->callFinder($type, [$query, $options]);
+ }
+
+ throw new BadMethodCallException(sprintf(
+ 'Unknown finder method "%s" on %s.',
+ $type,
+ static::class
+ ));
+ }
+
+ /**
+ * Provides the dynamic findBy and findAllBy methods.
+ *
+ * @param string $method The method name that was fired.
+ * @param array $args List of arguments passed to the function.
+ * @return \Cake\ORM\Query
+ * @throws \BadMethodCallException when there are missing arguments, or when
+ * and & or are combined.
+ */
+ protected function _dynamicFinder(string $method, array $args)
+ {
+ $method = Inflector::underscore($method);
+ preg_match('/^find_([\w]+)_by_/', $method, $matches);
+ if (empty($matches)) {
+ // find_by_ is 8 characters.
+ $fields = substr($method, 8);
+ $findType = 'all';
+ } else {
+ $fields = substr($method, strlen($matches[0]));
+ $findType = Inflector::variable($matches[1]);
+ }
+ $hasOr = strpos($fields, '_or_');
+ $hasAnd = strpos($fields, '_and_');
+
+ $makeConditions = function ($fields, $args) {
+ $conditions = [];
+ if (count($args) < count($fields)) {
+ throw new BadMethodCallException(sprintf(
+ 'Not enough arguments for magic finder. Got %s required %s',
+ count($args),
+ count($fields)
+ ));
+ }
+ foreach ($fields as $field) {
+ $conditions[$this->aliasField($field)] = array_shift($args);
+ }
+
+ return $conditions;
+ };
+
+ if ($hasOr !== false && $hasAnd !== false) {
+ throw new BadMethodCallException(
+ 'Cannot mix "and" & "or" in a magic finder. Use find() instead.'
+ );
+ }
+
+ if ($hasOr === false && $hasAnd === false) {
+ $conditions = $makeConditions([$fields], $args);
+ } elseif ($hasOr !== false) {
+ $fields = explode('_or_', $fields);
+ $conditions = [
+ 'OR' => $makeConditions($fields, $args),
+ ];
+ } else {
+ $fields = explode('_and_', $fields);
+ $conditions = $makeConditions($fields, $args);
+ }
+
+ return $this->find($findType, [
+ 'conditions' => $conditions,
+ ]);
+ }
+
+ /**
+ * Handles behavior delegation + dynamic finders.
+ *
+ * If your Table uses any behaviors you can call them as if
+ * they were on the table object.
+ *
+ * @param string $method name of the method to be invoked
+ * @param array $args List of arguments passed to the function
+ * @return mixed
+ * @throws \BadMethodCallException
+ */
+ public function __call($method, $args)
+ {
+ if ($this->_behaviors->hasMethod($method)) {
+ return $this->_behaviors->call($method, $args);
+ }
+ if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
+ return $this->_dynamicFinder($method, $args);
+ }
+
+ throw new BadMethodCallException(
+ sprintf('Unknown method "%s" called on %s', $method, static::class)
+ );
+ }
+
+ /**
+ * Returns the association named after the passed value if exists, otherwise
+ * throws an exception.
+ *
+ * @param string $property the association name
+ * @return \Cake\ORM\Association
+ * @throws \RuntimeException if no association with such name exists
+ */
+ public function __get($property)
+ {
+ $association = $this->_associations->get($property);
+ if (!$association) {
+ throw new RuntimeException(sprintf(
+ 'Undefined property `%s`. ' .
+ 'You have not defined the `%s` association on `%s`.',
+ $property,
+ $property,
+ static::class
+ ));
+ }
+
+ return $association;
+ }
+
+ /**
+ * Returns whether an association named after the passed value
+ * exists for this table.
+ *
+ * @param string $property the association name
+ * @return bool
+ */
+ public function __isset($property)
+ {
+ return $this->_associations->has($property);
+ }
+
+ /**
+ * Get the object used to marshal/convert array data into objects.
+ *
+ * Override this method if you want a table object to use custom
+ * marshalling logic.
+ *
+ * @return \Cake\ORM\Marshaller
+ * @see \Cake\ORM\Marshaller
+ */
+ public function marshaller(): Marshaller
+ {
+ return new Marshaller($this);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return \Cake\Datasource\EntityInterface
+ */
+ public function newEmptyEntity(): EntityInterface
+ {
+ $class = $this->getEntityClass();
+
+ return new $class([], ['source' => $this->getRegistryAlias()]);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * By default all the associations on this table will be hydrated. You can
+ * limit which associations are built, or include deeper associations
+ * using the options parameter:
+ *
+ * ```
+ * $article = $this->Articles->newEntity(
+ * $this->request->getData(),
+ * ['associated' => ['Tags', 'Comments.Users']]
+ * );
+ * ```
+ *
+ * You can limit fields that will be present in the constructed entity by
+ * passing the `fields` option, which is also accepted for associations:
+ *
+ * ```
+ * $article = $this->Articles->newEntity($this->request->getData(), [
+ * 'fields' => ['title', 'body', 'tags', 'comments'],
+ * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
+ * ]
+ * );
+ * ```
+ *
+ * The `fields` option lets remove or restrict input data from ending up in
+ * the entity. If you'd like to relax the entity's default accessible fields,
+ * you can use the `accessibleFields` option:
+ *
+ * ```
+ * $article = $this->Articles->newEntity(
+ * $this->request->getData(),
+ * ['accessibleFields' => ['protected_field' => true]]
+ * );
+ * ```
+ *
+ * By default, the data is validated before being passed to the new entity. In
+ * the case of invalid fields, those will not be present in the resulting object.
+ * The `validate` option can be used to disable validation on the passed data:
+ *
+ * ```
+ * $article = $this->Articles->newEntity(
+ * $this->request->getData(),
+ * ['validate' => false]
+ * );
+ * ```
+ *
+ * You can also pass the name of the validator to use in the `validate` option.
+ * If `null` is passed to the first param of this function, no validation will
+ * be performed.
+ *
+ * You can use the `Model.beforeMarshal` event to modify request data
+ * before it is converted into entities.
+ *
+ * @param array $data The data to build an entity with.
+ * @param array $options A list of options for the object hydration.
+ * @return \Cake\Datasource\EntityInterface
+ * @see \Cake\ORM\Marshaller::one()
+ */
+ public function newEntity(array $data, array $options = []): EntityInterface
+ {
+ if (!isset($options['associated'])) {
+ $options['associated'] = $this->_associations->keys();
+ }
+ $marshaller = $this->marshaller();
+
+ return $marshaller->one($data, $options);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * By default all the associations on this table will be hydrated. You can
+ * limit which associations are built, or include deeper associations
+ * using the options parameter:
+ *
+ * ```
+ * $articles = $this->Articles->newEntities(
+ * $this->request->getData(),
+ * ['associated' => ['Tags', 'Comments.Users']]
+ * );
+ * ```
+ *
+ * You can limit fields that will be present in the constructed entities by
+ * passing the `fields` option, which is also accepted for associations:
+ *
+ * ```
+ * $articles = $this->Articles->newEntities($this->request->getData(), [
+ * 'fields' => ['title', 'body', 'tags', 'comments'],
+ * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
+ * ]
+ * );
+ * ```
+ *
+ * You can use the `Model.beforeMarshal` event to modify request data
+ * before it is converted into entities.
+ *
+ * @param array $data The data to build an entity with.
+ * @param array $options A list of options for the objects hydration.
+ * @return array<\Cake\Datasource\EntityInterface> An array of hydrated records.
+ */
+ public function newEntities(array $data, array $options = []): array
+ {
+ if (!isset($options['associated'])) {
+ $options['associated'] = $this->_associations->keys();
+ }
+ $marshaller = $this->marshaller();
+
+ return $marshaller->many($data, $options);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * When merging HasMany or BelongsToMany associations, all the entities in the
+ * `$data` array will appear, those that can be matched by primary key will get
+ * the data merged, but those that cannot, will be discarded.
+ *
+ * You can limit fields that will be present in the merged entity by
+ * passing the `fields` option, which is also accepted for associations:
+ *
+ * ```
+ * $article = $this->Articles->patchEntity($article, $this->request->getData(), [
+ * 'fields' => ['title', 'body', 'tags', 'comments'],
+ * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
+ * ]
+ * );
+ * ```
+ *
+ * ```
+ * $article = $this->Articles->patchEntity($article, $this->request->getData(), [
+ * 'associated' => [
+ * 'Tags' => ['accessibleFields' => ['*' => true]]
+ * ]
+ * ]);
+ * ```
+ *
+ * By default, the data is validated before being passed to the entity. In
+ * the case of invalid fields, those will not be assigned to the entity.
+ * The `validate` option can be used to disable validation on the passed data:
+ *
+ * ```
+ * $article = $this->patchEntity($article, $this->request->getData(),[
+ * 'validate' => false
+ * ]);
+ * ```
+ *
+ * You can use the `Model.beforeMarshal` event to modify request data
+ * before it is converted into entities.
+ *
+ * When patching scalar values (null/booleans/string/integer/float), if the property
+ * presently has an identical value, the setter will not be called, and the
+ * property will not be marked as dirty. This is an optimization to prevent unnecessary field
+ * updates when persisting entities.
+ *
+ * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
+ * data merged in
+ * @param array $data key value list of fields to be merged into the entity
+ * @param array $options A list of options for the object hydration.
+ * @return \Cake\Datasource\EntityInterface
+ * @see \Cake\ORM\Marshaller::merge()
+ */
+ public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface
+ {
+ if (!isset($options['associated'])) {
+ $options['associated'] = $this->_associations->keys();
+ }
+ $marshaller = $this->marshaller();
+
+ return $marshaller->merge($entity, $data, $options);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Those entries in `$entities` that cannot be matched to any record in
+ * `$data` will be discarded. Records in `$data` that could not be matched will
+ * be marshalled as a new entity.
+ *
+ * When merging HasMany or BelongsToMany associations, all the entities in the
+ * `$data` array will appear, those that can be matched by primary key will get
+ * the data merged, but those that cannot, will be discarded.
+ *
+ * You can limit fields that will be present in the merged entities by
+ * passing the `fields` option, which is also accepted for associations:
+ *
+ * ```
+ * $articles = $this->Articles->patchEntities($articles, $this->request->getData(), [
+ * 'fields' => ['title', 'body', 'tags', 'comments'],
+ * 'associated' => ['Tags', 'Comments.Users' => ['fields' => 'username']]
+ * ]
+ * );
+ * ```
+ *
+ * You can use the `Model.beforeMarshal` event to modify request data
+ * before it is converted into entities.
+ *
+ * @param array<\Cake\Datasource\EntityInterface>|\Traversable $entities the entities that will get the
+ * data merged in
+ * @param array $data list of arrays to be merged into the entities
+ * @param array $options A list of options for the objects hydration.
+ * @return array<\Cake\Datasource\EntityInterface>
+ */
+ public function patchEntities(iterable $entities, array $data, array $options = []): array
+ {
+ if (!isset($options['associated'])) {
+ $options['associated'] = $this->_associations->keys();
+ }
+ $marshaller = $this->marshaller();
+
+ return $marshaller->mergeMany($entities, $data, $options);
+ }
+
+ /**
+ * Validator method used to check the uniqueness of a value for a column.
+ * This is meant to be used with the validation API and not to be called
+ * directly.
+ *
+ * ### Example:
+ *
+ * ```
+ * $validator->add('email', [
+ * 'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
+ * ])
+ * ```
+ *
+ * Unique validation can be scoped to the value of another column:
+ *
+ * ```
+ * $validator->add('email', [
+ * 'unique' => [
+ * 'rule' => ['validateUnique', ['scope' => 'site_id']],
+ * 'provider' => 'table'
+ * ]
+ * ]);
+ * ```
+ *
+ * In the above example, the email uniqueness will be scoped to only rows having
+ * the same site_id. Scoping will only be used if the scoping field is present in
+ * the data to be validated.
+ *
+ * @param mixed $value The value of column to be checked for uniqueness.
+ * @param array $options The options array, optionally containing the 'scope' key.
+ * May also be the validation context, if there are no options.
+ * @param array|null $context Either the validation context or null.
+ * @return bool True if the value is unique, or false if a non-scalar, non-unique value was given.
+ */
+ public function validateUnique($value, array $options, ?array $context = null): bool
+ {
+ if ($context === null) {
+ $context = $options;
+ }
+ $entity = new Entity(
+ $context['data'],
+ [
+ 'useSetters' => false,
+ 'markNew' => $context['newRecord'],
+ 'source' => $this->getRegistryAlias(),
+ ]
+ );
+ $fields = array_merge(
+ [$context['field']],
+ isset($options['scope']) ? (array)$options['scope'] : []
+ );
+ $values = $entity->extract($fields);
+ foreach ($values as $field) {
+ if ($field !== null && !is_scalar($field)) {
+ return false;
+ }
+ }
+ $class = static::IS_UNIQUE_CLASS;
+ /** @var \Cake\ORM\Rule\IsUnique $rule */
+ $rule = new $class($fields, $options);
+
+ return $rule($entity, ['repository' => $this]);
+ }
+
+ /**
+ * Get the Model callbacks this table is interested in.
+ *
+ * By implementing the conventional methods a table class is assumed
+ * to be interested in the related event.
+ *
+ * Override this method if you need to add non-conventional event listeners.
+ * Or if you want you table to listen to non-standard events.
+ *
+ * The conventional method map is:
+ *
+ * - Model.beforeMarshal => beforeMarshal
+ * - Model.afterMarshal => afterMarshal
+ * - Model.buildValidator => buildValidator
+ * - Model.beforeFind => beforeFind
+ * - Model.beforeSave => beforeSave
+ * - Model.afterSave => afterSave
+ * - Model.afterSaveCommit => afterSaveCommit
+ * - Model.beforeDelete => beforeDelete
+ * - Model.afterDelete => afterDelete
+ * - Model.afterDeleteCommit => afterDeleteCommit
+ * - Model.beforeRules => beforeRules
+ * - Model.afterRules => afterRules
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ $eventMap = [
+ 'Model.beforeMarshal' => 'beforeMarshal',
+ 'Model.afterMarshal' => 'afterMarshal',
+ 'Model.buildValidator' => 'buildValidator',
+ 'Model.beforeFind' => 'beforeFind',
+ 'Model.beforeSave' => 'beforeSave',
+ 'Model.afterSave' => 'afterSave',
+ 'Model.afterSaveCommit' => 'afterSaveCommit',
+ 'Model.beforeDelete' => 'beforeDelete',
+ 'Model.afterDelete' => 'afterDelete',
+ 'Model.afterDeleteCommit' => 'afterDeleteCommit',
+ 'Model.beforeRules' => 'beforeRules',
+ 'Model.afterRules' => 'afterRules',
+ ];
+ $events = [];
+
+ foreach ($eventMap as $event => $method) {
+ if (!method_exists($this, $method)) {
+ continue;
+ }
+ $events[$event] = $method;
+ }
+
+ return $events;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
+ * @return \Cake\ORM\RulesChecker
+ */
+ public function buildRules(RulesChecker $rules): RulesChecker
+ {
+ return $rules;
+ }
+
+ /**
+ * Gets a SaveOptionsBuilder instance.
+ *
+ * @param array $options Options to parse by the builder.
+ * @return \Cake\ORM\SaveOptionsBuilder
+ */
+ public function getSaveOptionsBuilder(array $options = []): SaveOptionsBuilder
+ {
+ return new SaveOptionsBuilder($this, $options);
+ }
+
+ /**
+ * Loads the specified associations in the passed entity or list of entities
+ * by executing extra queries in the database and merging the results in the
+ * appropriate properties.
+ *
+ * ### Example:
+ *
+ * ```
+ * $user = $usersTable->get(1);
+ * $user = $usersTable->loadInto($user, ['Articles.Tags', 'Articles.Comments']);
+ * echo $user->articles[0]->title;
+ * ```
+ *
+ * You can also load associations for multiple entities at once
+ *
+ * ### Example:
+ *
+ * ```
+ * $users = $usersTable->find()->where([...])->toList();
+ * $users = $usersTable->loadInto($users, ['Articles.Tags', 'Articles.Comments']);
+ * echo $user[1]->articles[0]->title;
+ * ```
+ *
+ * The properties for the associations to be loaded will be overwritten on each entity.
+ *
+ * @param \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface> $entities a single entity or list of entities
+ * @param array $contain A `contain()` compatible array.
+ * @see \Cake\ORM\Query::contain()
+ * @return \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface>
+ */
+ public function loadInto($entities, array $contain)
+ {
+ return (new LazyEagerLoader())->loadInto($entities, $contain, $this);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function validationMethodExists(string $name): bool
+ {
+ return method_exists($this, $name) || $this->behaviors()->hasMethod($name);
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ $conn = $this->getConnection();
+
+ return [
+ 'registryAlias' => $this->getRegistryAlias(),
+ 'table' => $this->getTable(),
+ 'alias' => $this->getAlias(),
+ 'entityClass' => $this->getEntityClass(),
+ 'associations' => $this->_associations->keys(),
+ 'behaviors' => $this->_behaviors->loaded(),
+ 'defaultConnection' => static::defaultConnectionName(),
+ 'connectionName' => $conn->configName(),
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/TableRegistry.php b/app/vendor/cakephp/cakephp/src/ORM/TableRegistry.php
new file mode 100644
index 000000000..52a0c42ae
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/TableRegistry.php
@@ -0,0 +1,145 @@
+setConfig('Users', ['table' => 'my_users']);
+ *
+ * // Prior to 3.6.0
+ * TableRegistry::config('Users', ['table' => 'my_users']);
+ * ```
+ *
+ * Configuration data is stored *per alias* if you use the same table with
+ * multiple aliases you will need to set configuration multiple times.
+ *
+ * ### Getting instances
+ *
+ * You can fetch instances out of the registry through `TableLocator::get()`.
+ * One instance is stored per alias. Once an alias is populated the same
+ * instance will always be returned. This reduces the ORM memory cost and
+ * helps make cyclic references easier to solve.
+ *
+ * ```
+ * $table = TableRegistry::getTableLocator()->get('Users', $config);
+ *
+ * // Prior to 3.6.0
+ * $table = TableRegistry::get('Users', $config);
+ * ```
+ */
+class TableRegistry
+{
+ /**
+ * Returns a singleton instance of LocatorInterface implementation.
+ *
+ * @return \Cake\ORM\Locator\LocatorInterface
+ */
+ public static function getTableLocator(): LocatorInterface
+ {
+ /** @var \Cake\ORM\Locator\LocatorInterface */
+ return FactoryLocator::get('Table');
+ }
+
+ /**
+ * Sets singleton instance of LocatorInterface implementation.
+ *
+ * @param \Cake\ORM\Locator\LocatorInterface $tableLocator Instance of a locator to use.
+ * @return void
+ */
+ public static function setTableLocator(LocatorInterface $tableLocator): void
+ {
+ FactoryLocator::add('Table', $tableLocator);
+ }
+
+ /**
+ * Get a table instance from the registry.
+ *
+ * See options specification in {@link TableLocator::get()}.
+ *
+ * @param string $alias The alias name you want to get.
+ * @param array $options The options you want to build the table with.
+ * @return \Cake\ORM\Table
+ * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\TableLocator::get()} instead. Will be removed in 5.0.
+ */
+ public static function get(string $alias, array $options = []): Table
+ {
+ return static::getTableLocator()->get($alias, $options);
+ }
+
+ /**
+ * Check to see if an instance exists in the registry.
+ *
+ * @param string $alias The alias to check for.
+ * @return bool
+ * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\TableLocator::exists()} instead. Will be removed in 5.0
+ */
+ public static function exists(string $alias): bool
+ {
+ return static::getTableLocator()->exists($alias);
+ }
+
+ /**
+ * Set an instance.
+ *
+ * @param string $alias The alias to set.
+ * @param \Cake\ORM\Table $object The table to set.
+ * @return \Cake\ORM\Table
+ * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\TableLocator::set()} instead. Will be removed in 5.0
+ */
+ public static function set(string $alias, Table $object): Table
+ {
+ return static::getTableLocator()->set($alias, $object);
+ }
+
+ /**
+ * Removes an instance from the registry.
+ *
+ * @param string $alias The alias to remove.
+ * @return void
+ * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\TableLocator::remove()} instead. Will be removed in 5.0
+ */
+ public static function remove(string $alias): void
+ {
+ static::getTableLocator()->remove($alias);
+ }
+
+ /**
+ * Clears the registry of configuration and instances.
+ *
+ * @return void
+ * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\TableLocator::clear()} instead. Will be removed in 5.0
+ */
+ public static function clear(): void
+ {
+ static::getTableLocator()->clear();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/ORM/composer.json b/app/vendor/cakephp/cakephp/src/ORM/composer.json
new file mode 100644
index 000000000..c0f2675d5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/ORM/composer.json
@@ -0,0 +1,44 @@
+{
+ "name": "cakephp/orm",
+ "description": "CakePHP ORM - Provides a flexible and powerful ORM implementing a data-mapper pattern.",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "orm",
+ "data-mapper",
+ "data-mapper pattern"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/orm/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/orm"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/collection": "^4.0",
+ "cakephp/core": "^4.0",
+ "cakephp/datasource": "^4.0",
+ "cakephp/database": "^4.0",
+ "cakephp/event": "^4.0",
+ "cakephp/utility": "^4.0",
+ "cakephp/validation": "^4.0"
+ },
+ "suggest": {
+ "cakephp/cache": "If you decide to use Query caching.",
+ "cakephp/i18n": "If you are using Translate/TimestampBehavior or Chronos types."
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\ORM\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Asset.php b/app/vendor/cakephp/cakephp/src/Routing/Asset.php
new file mode 100644
index 000000000..2e5e1215c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Asset.php
@@ -0,0 +1,372 @@
+ 1) {
+ $plugin = implode('/', [$plugin, Inflector::camelize($segments[1])]);
+ unset($segments[1]);
+ }
+ if (Plugin::isLoaded($plugin)) {
+ unset($segments[0]);
+ $pluginPath = Plugin::path($plugin)
+ . 'webroot'
+ . DIRECTORY_SEPARATOR
+ . implode(DIRECTORY_SEPARATOR, $segments);
+ if (is_file($pluginPath)) {
+ return $path . '?' . filemtime($pluginPath);
+ }
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * Checks if a file exists when theme is used, if no file is found default location is returned.
+ *
+ * ### Options:
+ *
+ * - `theme` Optional theme name
+ *
+ * @param string $file The file to create a webroot path to.
+ * @param array $options Options array.
+ * @return string Web accessible path to file.
+ */
+ public static function webroot(string $file, array $options = []): string
+ {
+ $options += ['theme' => null];
+ $requestWebroot = static::requestWebroot();
+
+ $asset = explode('?', $file);
+ $asset[1] = isset($asset[1]) ? '?' . $asset[1] : '';
+ $webPath = $requestWebroot . $asset[0];
+ $file = $asset[0];
+
+ $themeName = $options['theme'];
+ if ($themeName) {
+ $file = trim($file, '/');
+ $theme = static::inflectString($themeName) . '/';
+
+ if (DIRECTORY_SEPARATOR === '\\') {
+ $file = str_replace('/', '\\', $file);
+ }
+
+ if (is_file(Configure::read('App.wwwRoot') . $theme . $file)) {
+ $webPath = $requestWebroot . $theme . $asset[0];
+ } else {
+ $themePath = Plugin::path($themeName);
+ $path = $themePath . 'webroot/' . $file;
+ if (is_file($path)) {
+ $webPath = $requestWebroot . $theme . $asset[0];
+ }
+ }
+ }
+ if (strpos($webPath, '//') !== false) {
+ return str_replace('//', '/', $webPath . $asset[1]);
+ }
+
+ return $webPath . $asset[1];
+ }
+
+ /**
+ * Inflect the theme/plugin name to type set using `Asset::setInflectionType()`.
+ *
+ * @param string $string String inflected.
+ * @return string Inflected name of the theme
+ */
+ protected static function inflectString(string $string): string
+ {
+ return Inflector::{static::$inflectionType}($string);
+ }
+
+ /**
+ * Get webroot from request.
+ *
+ * @return string
+ */
+ protected static function requestWebroot(): string
+ {
+ $request = Router::getRequest();
+ if ($request === null) {
+ return '/';
+ }
+
+ return $request->getAttribute('webroot');
+ }
+
+ /**
+ * Splits a dot syntax plugin name into its plugin and filename.
+ * If $name does not have a dot, then index 0 will be null.
+ * It checks if the plugin is loaded, else filename will stay unchanged for filenames containing dot.
+ *
+ * @param string $name The name you want to plugin split.
+ * @return array Array with 2 indexes. 0 => plugin name, 1 => filename.
+ * @psalm-return array{string|null, string}
+ */
+ protected static function pluginSplit(string $name): array
+ {
+ $plugin = null;
+ [$first, $second] = pluginSplit($name);
+ if ($first && Plugin::isLoaded($first)) {
+ $name = $second;
+ $plugin = $first;
+ }
+
+ return [$plugin, $name];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Exception/DuplicateNamedRouteException.php b/app/vendor/cakephp/cakephp/src/Routing/Exception/DuplicateNamedRouteException.php
new file mode 100644
index 000000000..4bff190d6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Exception/DuplicateNamedRouteException.php
@@ -0,0 +1,45 @@
+_messageTemplate = $message['message'];
+ }
+ parent::__construct($message, $code, $previous);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php b/app/vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php
new file mode 100644
index 000000000..e6456fea4
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Exception/MissingControllerException.php
@@ -0,0 +1,10 @@
+_messageTemplate = $message['message'];
+ } elseif (isset($message['method']) && $message['method']) {
+ $this->_messageTemplate = $this->_messageTemplateWithMethod;
+ }
+ }
+ parent::__construct($message, $code, $previous);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Exception/RedirectException.php b/app/vendor/cakephp/cakephp/src/Routing/Exception/RedirectException.php
new file mode 100644
index 000000000..ff1beffdc
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Exception/RedirectException.php
@@ -0,0 +1,42 @@
+cacheTime = $options['cacheTime'];
+ }
+ }
+
+ /**
+ * Serve assets if the path matches one.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $url = $request->getUri()->getPath();
+ if (strpos($url, '..') !== false || strpos($url, '.') === false) {
+ return $handler->handle($request);
+ }
+
+ if (strpos($url, '/.') !== false) {
+ return $handler->handle($request);
+ }
+
+ $assetFile = $this->_getAssetFile($url);
+ if ($assetFile === null || !is_file($assetFile)) {
+ return $handler->handle($request);
+ }
+
+ $file = new SplFileInfo($assetFile);
+ $modifiedTime = $file->getMTime();
+ if ($this->isNotModified($request, $file)) {
+ return (new Response())
+ ->withStringBody('')
+ ->withStatus(304)
+ ->withHeader(
+ 'Last-Modified',
+ date(DATE_RFC850, $modifiedTime)
+ );
+ }
+
+ return $this->deliverAsset($request, $file);
+ }
+
+ /**
+ * Check the not modified header.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to check.
+ * @param \SplFileInfo $file The file object to compare.
+ * @return bool
+ */
+ protected function isNotModified(ServerRequestInterface $request, SplFileInfo $file): bool
+ {
+ $modifiedSince = $request->getHeaderLine('If-Modified-Since');
+ if (!$modifiedSince) {
+ return false;
+ }
+
+ return strtotime($modifiedSince) === $file->getMTime();
+ }
+
+ /**
+ * Builds asset file path based off url
+ *
+ * @param string $url Asset URL
+ * @return string|null Absolute path for asset file, null on failure
+ */
+ protected function _getAssetFile(string $url): ?string
+ {
+ $parts = explode('/', ltrim($url, '/'));
+ $pluginPart = [];
+ for ($i = 0; $i < 2; $i++) {
+ if (!isset($parts[$i])) {
+ break;
+ }
+ $pluginPart[] = Inflector::camelize($parts[$i]);
+ $plugin = implode('/', $pluginPart);
+ if (Plugin::isLoaded($plugin)) {
+ $parts = array_slice($parts, $i + 1);
+ $fileFragment = implode(DIRECTORY_SEPARATOR, $parts);
+ $pluginWebroot = Plugin::path($plugin) . 'webroot' . DIRECTORY_SEPARATOR;
+
+ return $pluginWebroot . $fileFragment;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sends an asset file to the client
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request object to use.
+ * @param \SplFileInfo $file The file wrapper for the file.
+ * @return \Cake\Http\Response The response with the file & headers.
+ */
+ protected function deliverAsset(ServerRequestInterface $request, SplFileInfo $file): Response
+ {
+ $stream = new Stream(fopen($file->getPathname(), 'rb'));
+
+ $response = new Response(['stream' => $stream]);
+
+ $contentType = $response->getMimeType($file->getExtension()) ?: 'application/octet-stream';
+ $modified = $file->getMTime();
+ $expire = strtotime($this->cacheTime);
+ $maxAge = $expire - time();
+
+ return $response
+ ->withHeader('Content-Type', $contentType)
+ ->withHeader('Cache-Control', 'public,max-age=' . $maxAge)
+ ->withHeader('Date', gmdate(DATE_RFC7231, time()))
+ ->withHeader('Last-Modified', gmdate(DATE_RFC7231, $modified))
+ ->withHeader('Expires', gmdate(DATE_RFC7231, $expire));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php b/app/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php
new file mode 100644
index 000000000..66d56a80b
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php
@@ -0,0 +1,169 @@
+app = $app;
+ $this->cacheConfig = $cacheConfig;
+ }
+
+ /**
+ * Trigger the application's routes() hook if the application exists and Router isn't initialized.
+ * Uses the routes cache if enabled via configuration param "Router.cache"
+ *
+ * If the middleware is created without an Application, routes will be
+ * loaded via the automatic route loading that pre-dates the routes() hook.
+ *
+ * @return void
+ */
+ protected function loadRoutes(): void
+ {
+ $routeCollection = $this->buildRouteCollection();
+ Router::setRouteCollection($routeCollection);
+ }
+
+ /**
+ * Check if route cache is enabled and use the configured Cache to 'remember' the route collection
+ *
+ * @return \Cake\Routing\RouteCollection
+ */
+ protected function buildRouteCollection(): RouteCollection
+ {
+ if (Cache::enabled() && $this->cacheConfig !== null) {
+ return Cache::remember(static::ROUTE_COLLECTION_CACHE_KEY, function () {
+ return $this->prepareRouteCollection();
+ }, $this->cacheConfig);
+ }
+
+ return $this->prepareRouteCollection();
+ }
+
+ /**
+ * Generate the route collection using the builder
+ *
+ * @return \Cake\Routing\RouteCollection
+ */
+ protected function prepareRouteCollection(): RouteCollection
+ {
+ $builder = Router::createRouteBuilder('/');
+ $this->app->routes($builder);
+ if ($this->app instanceof PluginApplicationInterface) {
+ $this->app->pluginRoutes($builder);
+ }
+
+ return Router::getRouteCollection();
+ }
+
+ /**
+ * Apply routing and update the request.
+ *
+ * Any route/path specific middleware will be wrapped around $next and then the new middleware stack will be
+ * invoked.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
+ * @return \Psr\Http\Message\ResponseInterface A response.
+ */
+ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
+ {
+ $this->loadRoutes();
+ try {
+ Router::setRequest($request);
+ $params = (array)$request->getAttribute('params', []);
+ $middleware = [];
+ if (empty($params['controller'])) {
+ $params = Router::parseRequest($request) + $params;
+ if (isset($params['_middleware'])) {
+ $middleware = $params['_middleware'];
+ unset($params['_middleware']);
+ }
+ /** @var \Cake\Http\ServerRequest $request */
+ $request = $request->withAttribute('params', $params);
+ Router::setRequest($request);
+ }
+ } catch (RedirectException $e) {
+ return new RedirectResponse(
+ $e->getMessage(),
+ $e->getCode()
+ );
+ } catch (DeprecatedRedirectException $e) {
+ return new RedirectResponse(
+ $e->getMessage(),
+ $e->getCode()
+ );
+ }
+ $matching = Router::getRouteCollection()->getMiddleware($middleware);
+ if (!$matching) {
+ return $handler->handle($request);
+ }
+
+ $middleware = new MiddlewareQueue($matching);
+ $runner = new Runner();
+
+ return $runner->run($middleware, $request, $handler);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/DashedRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/DashedRoute.php
new file mode 100644
index 000000000..8a0fb1383
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Route/DashedRoute.php
@@ -0,0 +1,124 @@
+ 'MyPlugin', 'controller' => 'MyController', 'action' => 'myAction']`
+ */
+class DashedRoute extends Route
+{
+ /**
+ * Flag for tracking whether or not the defaults have been inflected.
+ *
+ * Default values need to be inflected so that they match the inflections that
+ * match() will create.
+ *
+ * @var bool
+ */
+ protected $_inflectedDefaults = false;
+
+ /**
+ * Camelizes the previously dashed plugin route taking into account plugin vendors
+ *
+ * @param string $plugin Plugin name
+ * @return string
+ */
+ protected function _camelizePlugin(string $plugin): string
+ {
+ $plugin = str_replace('-', '_', $plugin);
+ if (strpos($plugin, '/') === false) {
+ return Inflector::camelize($plugin);
+ }
+ [$vendor, $plugin] = explode('/', $plugin, 2);
+
+ return Inflector::camelize($vendor) . '/' . Inflector::camelize($plugin);
+ }
+
+ /**
+ * Parses a string URL into an array. If it matches, it will convert the
+ * controller and plugin keys to their CamelCased form and action key to
+ * camelBacked form.
+ *
+ * @param string $url The URL to parse
+ * @param string $method The HTTP method.
+ * @return array|null An array of request parameters, or null on failure.
+ */
+ public function parse(string $url, string $method = ''): ?array
+ {
+ $params = parent::parse($url, $method);
+ if (!$params) {
+ return null;
+ }
+ if (!empty($params['controller'])) {
+ $params['controller'] = Inflector::camelize($params['controller'], '-');
+ }
+ if (!empty($params['plugin'])) {
+ $params['plugin'] = $this->_camelizePlugin($params['plugin']);
+ }
+ if (!empty($params['action'])) {
+ $params['action'] = Inflector::variable(str_replace(
+ '-',
+ '_',
+ $params['action']
+ ));
+ }
+
+ return $params;
+ }
+
+ /**
+ * Dasherizes the controller, action and plugin params before passing them on
+ * to the parent class.
+ *
+ * @param array $url Array of parameters to convert to a string.
+ * @param array $context An array of the current request context.
+ * Contains information such as the current host, scheme, port, and base
+ * directory.
+ * @return string|null Either a string URL or null.
+ */
+ public function match(array $url, array $context = []): ?string
+ {
+ $url = $this->_dasherize($url);
+ if (!$this->_inflectedDefaults) {
+ $this->_inflectedDefaults = true;
+ $this->defaults = $this->_dasherize($this->defaults);
+ }
+
+ return parent::match($url, $context);
+ }
+
+ /**
+ * Helper method for dasherizing keys in a URL array.
+ *
+ * @param array $url An array of URL keys.
+ * @return array
+ */
+ protected function _dasherize(array $url): array
+ {
+ foreach (['controller', 'plugin', 'action'] as $element) {
+ if (!empty($url[$element])) {
+ $url[$element] = Inflector::dasherize($url[$element]);
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/EntityRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/EntityRoute.php
new file mode 100644
index 000000000..6adaedc65
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Route/EntityRoute.php
@@ -0,0 +1,82 @@
+_compiledRoute)) {
+ $this->compile();
+ }
+
+ if (isset($url['_entity'])) {
+ $entity = $url['_entity'];
+ $this->_checkEntity($entity);
+
+ foreach ($this->keys as $field) {
+ if (!isset($url[$field]) && isset($entity[$field])) {
+ $url[$field] = $entity[$field];
+ }
+ }
+ }
+
+ return parent::match($url, $context);
+ }
+
+ /**
+ * Checks that we really deal with an entity object
+ *
+ * @throws \RuntimeException
+ * @param \ArrayAccess|array $entity Entity value from the URL options
+ * @return void
+ */
+ protected function _checkEntity($entity): void
+ {
+ if (!$entity instanceof ArrayAccess && !is_array($entity)) {
+ throw new RuntimeException(sprintf(
+ 'Route `%s` expects the URL option `_entity` to be an array or object implementing \ArrayAccess, '
+ . 'but `%s` passed.',
+ $this->template,
+ getTypeName($entity)
+ ));
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/InflectedRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/InflectedRoute.php
new file mode 100644
index 000000000..852ed07b1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Route/InflectedRoute.php
@@ -0,0 +1,104 @@
+ 'MyController']`
+ */
+class InflectedRoute extends Route
+{
+ /**
+ * Flag for tracking whether or not the defaults have been inflected.
+ *
+ * Default values need to be inflected so that they match the inflections that match()
+ * will create.
+ *
+ * @var bool
+ */
+ protected $_inflectedDefaults = false;
+
+ /**
+ * Parses a string URL into an array. If it matches, it will convert the prefix, controller and
+ * plugin keys to their camelized form.
+ *
+ * @param string $url The URL to parse
+ * @param string $method The HTTP method being matched.
+ * @return array|null An array of request parameters, or null on failure.
+ */
+ public function parse(string $url, string $method = ''): ?array
+ {
+ $params = parent::parse($url, $method);
+ if (!$params) {
+ return null;
+ }
+ if (!empty($params['controller'])) {
+ $params['controller'] = Inflector::camelize($params['controller']);
+ }
+ if (!empty($params['plugin'])) {
+ if (strpos($params['plugin'], '/') === false) {
+ $params['plugin'] = Inflector::camelize($params['plugin']);
+ } else {
+ [$vendor, $plugin] = explode('/', $params['plugin'], 2);
+ $params['plugin'] = Inflector::camelize($vendor) . '/' . Inflector::camelize($plugin);
+ }
+ }
+
+ return $params;
+ }
+
+ /**
+ * Underscores the prefix, controller and plugin params before passing them on to the
+ * parent class
+ *
+ * @param array $url Array of parameters to convert to a string.
+ * @param array $context An array of the current request context.
+ * Contains information such as the current host, scheme, port, and base
+ * directory.
+ * @return string|null Either a string URL for the parameters if they match or null.
+ */
+ public function match(array $url, array $context = []): ?string
+ {
+ $url = $this->_underscore($url);
+ if (!$this->_inflectedDefaults) {
+ $this->_inflectedDefaults = true;
+ $this->defaults = $this->_underscore($this->defaults);
+ }
+
+ return parent::match($url, $context);
+ }
+
+ /**
+ * Helper method for underscoring keys in a URL array.
+ *
+ * @param array $url An array of URL keys.
+ * @return array
+ */
+ protected function _underscore(array $url): array
+ {
+ if (!empty($url['controller'])) {
+ $url['controller'] = Inflector::underscore($url['controller']);
+ }
+ if (!empty($url['plugin'])) {
+ $url['plugin'] = Inflector::underscore($url['plugin']);
+ }
+
+ return $url;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/PluginShortRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/PluginShortRoute.php
new file mode 100644
index 000000000..b3e1d6fda
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Route/PluginShortRoute.php
@@ -0,0 +1,65 @@
+defaults['controller'] = $url['controller'];
+ $result = parent::match($url, $context);
+ unset($this->defaults['controller']);
+
+ return $result;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/RedirectRoute.php b/app/vendor/cakephp/cakephp/src/Routing/Route/RedirectRoute.php
new file mode 100644
index 000000000..3be731f98
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Route/RedirectRoute.php
@@ -0,0 +1,117 @@
+value array or a CakePHP array URL.
+ * @param array $options Array of additional options for the Route
+ */
+ public function __construct(string $template, array $defaults = [], array $options = [])
+ {
+ parent::__construct($template, $defaults, $options);
+ if (isset($defaults['redirect'])) {
+ $defaults = (array)$defaults['redirect'];
+ }
+ $this->redirect = $defaults;
+ }
+
+ /**
+ * Parses a string URL into an array. Parsed URLs will result in an automatic
+ * redirection.
+ *
+ * @param string $url The URL to parse.
+ * @param string $method The HTTP method being used.
+ * @return array|null Null on failure. An exception is raised on a successful match. Array return type is unused.
+ * @throws \Cake\Http\Exception\RedirectException An exception is raised on successful match.
+ * This is used to halt route matching and signal to the middleware that a redirect should happen.
+ */
+ public function parse(string $url, string $method = ''): ?array
+ {
+ $params = parent::parse($url, $method);
+ if (!$params) {
+ return null;
+ }
+ $redirect = $this->redirect;
+ if ($this->redirect && count($this->redirect) === 1 && !isset($this->redirect['controller'])) {
+ $redirect = $this->redirect[0];
+ }
+ if (isset($this->options['persist']) && is_array($redirect)) {
+ $redirect += ['pass' => $params['pass'], 'url' => []];
+ if (is_array($this->options['persist'])) {
+ foreach ($this->options['persist'] as $elem) {
+ if (isset($params[$elem])) {
+ $redirect[$elem] = $params[$elem];
+ }
+ }
+ }
+ $redirect = Router::reverseToArray($redirect);
+ }
+ $status = 301;
+ if (isset($this->options['status']) && ($this->options['status'] >= 300 && $this->options['status'] < 400)) {
+ $status = $this->options['status'];
+ }
+ throw new RedirectException(Router::url($redirect, true), $status);
+ }
+
+ /**
+ * There is no reverse routing redirection routes.
+ *
+ * @param array $url Array of parameters to convert to a string.
+ * @param array $context Array of request context parameters.
+ * @return string|null Always null, string return result unused.
+ */
+ public function match(array $url, array $context = []): ?string
+ {
+ return null;
+ }
+
+ /**
+ * Sets the HTTP status
+ *
+ * @param int $status The status code for this route
+ * @return $this
+ */
+ public function setStatus(int $status)
+ {
+ $this->options['status'] = $status;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Route/Route.php b/app/vendor/cakephp/cakephp/src/Routing/Route/Route.php
new file mode 100644
index 000000000..d2bc64bb8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Route/Route.php
@@ -0,0 +1,923 @@
+template = $template;
+ $this->defaults = $defaults;
+ $this->options = $options + ['_ext' => [], '_middleware' => []];
+ $this->setExtensions((array)$this->options['_ext']);
+ $this->setMiddleware((array)$this->options['_middleware']);
+ unset($this->options['_middleware']);
+
+ if (isset($this->defaults['_method'])) {
+ $this->defaults['_method'] = $this->normalizeAndValidateMethods($this->defaults['_method']);
+ }
+ }
+
+ /**
+ * Set the supported extensions for this route.
+ *
+ * @param string[] $extensions The extensions to set.
+ * @return $this
+ */
+ public function setExtensions(array $extensions)
+ {
+ $this->_extensions = [];
+ foreach ($extensions as $ext) {
+ $this->_extensions[] = strtolower($ext);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the supported extensions for this route.
+ *
+ * @return string[]
+ */
+ public function getExtensions(): array
+ {
+ return $this->_extensions;
+ }
+
+ /**
+ * Set the accepted HTTP methods for this route.
+ *
+ * @param string[] $methods The HTTP methods to accept.
+ * @return $this
+ * @throws \InvalidArgumentException When methods are not in `VALID_METHODS` list.
+ */
+ public function setMethods(array $methods)
+ {
+ $this->defaults['_method'] = $this->normalizeAndValidateMethods($methods);
+
+ return $this;
+ }
+
+ /**
+ * Normalize method names to upper case and validate that they are valid HTTP methods.
+ *
+ * @param string|string[] $methods Methods.
+ * @return string|string[]
+ * @throws \InvalidArgumentException When methods are not in `VALID_METHODS` list.
+ */
+ protected function normalizeAndValidateMethods($methods)
+ {
+ $methods = is_array($methods)
+ ? array_map('strtoupper', $methods)
+ : strtoupper($methods);
+
+ $diff = array_diff((array)$methods, static::VALID_METHODS);
+ if ($diff !== []) {
+ throw new InvalidArgumentException(
+ sprintf('Invalid HTTP method received. `%s` is invalid.', implode(', ', $diff))
+ );
+ }
+
+ return $methods;
+ }
+
+ /**
+ * Set regexp patterns for routing parameters
+ *
+ * If any of your patterns contain multibyte values, the `multibytePattern`
+ * mode will be enabled.
+ *
+ * @param string[] $patterns The patterns to apply to routing elements
+ * @return $this
+ */
+ public function setPatterns(array $patterns)
+ {
+ $patternValues = implode('', $patterns);
+ if (mb_strlen($patternValues) < strlen($patternValues)) {
+ $this->options['multibytePattern'] = true;
+ }
+ $this->options = $patterns + $this->options;
+
+ return $this;
+ }
+
+ /**
+ * Set host requirement
+ *
+ * @param string $host The host name this route is bound to
+ * @return $this
+ */
+ public function setHost(string $host)
+ {
+ $this->options['_host'] = $host;
+
+ return $this;
+ }
+
+ /**
+ * Set the names of parameters that will be converted into passed parameters
+ *
+ * @param string[] $names The names of the parameters that should be passed.
+ * @return $this
+ */
+ public function setPass(array $names)
+ {
+ $this->options['pass'] = $names;
+
+ return $this;
+ }
+
+ /**
+ * Set the names of parameters that will persisted automatically
+ *
+ * Persistent parameters allow you to define which route parameters should be automatically
+ * included when generating new URLs. You can override persistent parameters
+ * by redefining them in a URL or remove them by setting the persistent parameter to `false`.
+ *
+ * ```
+ * // remove a persistent 'date' parameter
+ * Router::url(['date' => false', ...]);
+ * ```
+ *
+ * @param array $names The names of the parameters that should be passed.
+ * @return $this
+ */
+ public function setPersist(array $names)
+ {
+ $this->options['persist'] = $names;
+
+ return $this;
+ }
+
+ /**
+ * Check if a Route has been compiled into a regular expression.
+ *
+ * @return bool
+ */
+ public function compiled(): bool
+ {
+ return $this->_compiledRoute !== null;
+ }
+
+ /**
+ * Compiles the route's regular expression.
+ *
+ * Modifies defaults property so all necessary keys are set
+ * and populates $this->names with the named routing elements.
+ *
+ * @return string Returns a string regular expression of the compiled route.
+ */
+ public function compile(): string
+ {
+ if ($this->_compiledRoute === null) {
+ $this->_writeRoute();
+ }
+
+ /** @var string */
+ return $this->_compiledRoute;
+ }
+
+ /**
+ * Builds a route regular expression.
+ *
+ * Uses the template, defaults and options properties to compile a
+ * regular expression that can be used to parse request strings.
+ *
+ * @return void
+ */
+ protected function _writeRoute(): void
+ {
+ if (empty($this->template) || ($this->template === '/')) {
+ $this->_compiledRoute = '#^/*$#';
+ $this->keys = [];
+
+ return;
+ }
+ $route = $this->template;
+ $names = $routeParams = [];
+ $parsed = preg_quote($this->template, '#');
+
+ if (strpos($route, '{') !== false && strpos($route, '}') !== false) {
+ preg_match_all('/\{([a-z][a-z0-9-_]*)\}/i', $route, $namedElements);
+ $this->braceKeys = true;
+ } else {
+ preg_match_all('/:([a-z0-9-_]+(?braceKeys = false;
+ }
+ foreach ($namedElements[1] as $i => $name) {
+ $search = preg_quote($namedElements[0][$i]);
+ if (isset($this->options[$name])) {
+ $option = '';
+ if ($name !== 'plugin' && array_key_exists($name, $this->defaults)) {
+ $option = '?';
+ }
+ $slashParam = '/' . $search;
+ // phpcs:disable Generic.Files.LineLength
+ if (strpos($parsed, $slashParam) !== false) {
+ $routeParams[$slashParam] = '(?:/(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option;
+ } else {
+ $routeParams[$search] = '(?:(?P<' . $name . '>' . $this->options[$name] . ')' . $option . ')' . $option;
+ }
+ // phpcs:disable Generic.Files.LineLength
+ } else {
+ $routeParams[$search] = '(?:(?P<' . $name . '>[^/]+))';
+ }
+ $names[] = $name;
+ }
+ if (preg_match('#\/\*\*$#', $route)) {
+ $parsed = preg_replace('#/\\\\\*\\\\\*$#', '(?:/(?P<_trailing_>.*))?', $parsed);
+ $this->_greedy = true;
+ }
+ if (preg_match('#\/\*$#', $route)) {
+ $parsed = preg_replace('#/\\\\\*$#', '(?:/(?P<_args_>.*))?', $parsed);
+ $this->_greedy = true;
+ }
+ $mode = '';
+ if (!empty($this->options['multibytePattern'])) {
+ $mode = 'u';
+ }
+ krsort($routeParams);
+ $parsed = str_replace(array_keys($routeParams), $routeParams, $parsed);
+ $this->_compiledRoute = '#^' . $parsed . '[/]*$#' . $mode;
+ $this->keys = $names;
+
+ // Remove defaults that are also keys. They can cause match failures
+ foreach ($this->keys as $key) {
+ unset($this->defaults[$key]);
+ }
+
+ $keys = $this->keys;
+ sort($keys);
+ $this->keys = array_reverse($keys);
+ }
+
+ /**
+ * Get the standardized plugin.controller:action name for a route.
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ if (!empty($this->_name)) {
+ return $this->_name;
+ }
+ $name = '';
+ $keys = [
+ 'prefix' => ':',
+ 'plugin' => '.',
+ 'controller' => ':',
+ 'action' => '',
+ ];
+ foreach ($keys as $key => $glue) {
+ $value = null;
+ if (
+ strpos($this->template, ':' . $key) !== false
+ || strpos($this->template, '{' . $key . '}') !== false
+ ) {
+ $value = '_' . $key;
+ } elseif (isset($this->defaults[$key])) {
+ $value = $this->defaults[$key];
+ }
+
+ if ($value === null) {
+ continue;
+ }
+ if ($value === true || $value === false) {
+ $value = $value ? '1' : '0';
+ }
+ $name .= $value . $glue;
+ }
+
+ return $this->_name = strtolower($name);
+ }
+
+ /**
+ * Checks to see if the given URL can be parsed by this route.
+ *
+ * If the route can be parsed an array of parameters will be returned; if not
+ * false will be returned.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The URL to attempt to parse.
+ * @return array|null An array of request parameters, or null on failure.
+ */
+ public function parseRequest(ServerRequestInterface $request): ?array
+ {
+ $uri = $request->getUri();
+ if (isset($this->options['_host']) && !$this->hostMatches($uri->getHost())) {
+ return null;
+ }
+
+ return $this->parse($uri->getPath(), $request->getMethod());
+ }
+
+ /**
+ * Checks to see if the given URL can be parsed by this route.
+ *
+ * If the route can be parsed an array of parameters will be returned; if not
+ * false will be returned. String URLs are parsed if they match a routes regular expression.
+ *
+ * @param string $url The URL to attempt to parse.
+ * @param string $method The HTTP method of the request being parsed.
+ * @return array|null An array of request parameters, or null on failure.
+ * @throws \InvalidArgumentException When method is not an empty string or in `VALID_METHODS` list.
+ */
+ public function parse(string $url, string $method): ?array
+ {
+ if ($method !== '') {
+ $method = $this->normalizeAndValidateMethods($method);
+ }
+ $compiledRoute = $this->compile();
+ [$url, $ext] = $this->_parseExtension($url);
+
+ if (!preg_match($compiledRoute, urldecode($url), $route)) {
+ return null;
+ }
+
+ if (
+ isset($this->defaults['_method']) &&
+ !in_array($method, (array)$this->defaults['_method'], true)
+ ) {
+ return null;
+ }
+
+ array_shift($route);
+ $count = count($this->keys);
+ for ($i = 0; $i <= $count; $i++) {
+ unset($route[$i]);
+ }
+ $route['pass'] = [];
+
+ // Assign defaults, set passed args to pass
+ foreach ($this->defaults as $key => $value) {
+ if (isset($route[$key])) {
+ continue;
+ }
+ if (is_int($key)) {
+ $route['pass'][] = $value;
+ continue;
+ }
+ $route[$key] = $value;
+ }
+
+ if (isset($route['_args_'])) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $pass = $this->_parseArgs($route['_args_'], $route);
+ $route['pass'] = array_merge($route['pass'], $pass);
+ unset($route['_args_']);
+ }
+
+ if (isset($route['_trailing_'])) {
+ $route['pass'][] = $route['_trailing_'];
+ unset($route['_trailing_']);
+ }
+
+ if (!empty($ext)) {
+ $route['_ext'] = $ext;
+ }
+
+ // pass the name if set
+ if (isset($this->options['_name'])) {
+ $route['_name'] = $this->options['_name'];
+ }
+
+ // restructure 'pass' key route params
+ if (isset($this->options['pass'])) {
+ $j = count($this->options['pass']);
+ while ($j--) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ if (isset($route[$this->options['pass'][$j]])) {
+ array_unshift($route['pass'], $route[$this->options['pass'][$j]]);
+ }
+ }
+ }
+ $route['_matchedRoute'] = $this->template;
+ if (count($this->middleware) > 0) {
+ $route['_middleware'] = $this->middleware;
+ }
+
+ return $route;
+ }
+
+ /**
+ * Check to see if the host matches the route requirements
+ *
+ * @param string $host The request's host name
+ * @return bool Whether or not the host matches any conditions set in for this route.
+ */
+ public function hostMatches(string $host): bool
+ {
+ $pattern = '@^' . str_replace('\*', '.*', preg_quote($this->options['_host'], '@')) . '$@';
+
+ return preg_match($pattern, $host) !== 0;
+ }
+
+ /**
+ * Removes the extension from $url if it contains a registered extension.
+ * If no registered extension is found, no extension is returned and the URL is returned unmodified.
+ *
+ * @param string $url The url to parse.
+ * @return array containing url, extension
+ */
+ protected function _parseExtension(string $url): array
+ {
+ if (count($this->_extensions) && strpos($url, '.') !== false) {
+ foreach ($this->_extensions as $ext) {
+ $len = strlen($ext) + 1;
+ if (substr($url, -$len) === '.' . $ext) {
+ return [substr($url, 0, $len * -1), $ext];
+ }
+ }
+ }
+
+ return [$url, null];
+ }
+
+ /**
+ * Parse passed parameters into a list of passed args.
+ *
+ * Return true if a given named $param's $val matches a given $rule depending on $context.
+ * Currently implemented rule types are controller, action and match that can be combined with each other.
+ *
+ * @param string $args A string with the passed params. eg. /1/foo
+ * @param array $context The current route context, which should contain controller/action keys.
+ * @return string[] Array of passed args.
+ */
+ protected function _parseArgs(string $args, array $context): array
+ {
+ $pass = [];
+ $args = explode('/', $args);
+
+ foreach ($args as $param) {
+ if (empty($param) && $param !== '0') {
+ continue;
+ }
+ $pass[] = rawurldecode($param);
+ }
+
+ return $pass;
+ }
+
+ /**
+ * Apply persistent parameters to a URL array. Persistent parameters are a
+ * special key used during route creation to force route parameters to
+ * persist when omitted from a URL array.
+ *
+ * @param array $url The array to apply persistent parameters to.
+ * @param array $params An array of persistent values to replace persistent ones.
+ * @return array An array with persistent parameters applied.
+ */
+ protected function _persistParams(array $url, array $params): array
+ {
+ foreach ($this->options['persist'] as $persistKey) {
+ if (array_key_exists($persistKey, $params) && !isset($url[$persistKey])) {
+ $url[$persistKey] = $params[$persistKey];
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * Check if a URL array matches this route instance.
+ *
+ * If the URL matches the route parameters and settings, then
+ * return a generated string URL. If the URL doesn't match the route parameters, false will be returned.
+ * This method handles the reverse routing or conversion of URL arrays into string URLs.
+ *
+ * @param array $url An array of parameters to check matching with.
+ * @param array $context An array of the current request context.
+ * Contains information such as the current host, scheme, port, base
+ * directory and other url params.
+ * @return string|null Either a string URL for the parameters if they match or null.
+ */
+ public function match(array $url, array $context = []): ?string
+ {
+ if (empty($this->_compiledRoute)) {
+ $this->compile();
+ }
+ $defaults = $this->defaults;
+ $context += ['params' => [], '_port' => null, '_scheme' => null, '_host' => null];
+
+ if (
+ !empty($this->options['persist']) &&
+ is_array($this->options['persist'])
+ ) {
+ $url = $this->_persistParams($url, $context['params']);
+ }
+ unset($context['params']);
+ $hostOptions = array_intersect_key($url, $context);
+
+ // Apply the _host option if possible
+ if (isset($this->options['_host'])) {
+ if (!isset($hostOptions['_host']) && strpos($this->options['_host'], '*') === false) {
+ $hostOptions['_host'] = $this->options['_host'];
+ }
+ if (!isset($hostOptions['_host'])) {
+ $hostOptions['_host'] = $context['_host'];
+ }
+
+ // The host did not match the route preferences
+ if (!$this->hostMatches((string)$hostOptions['_host'])) {
+ return null;
+ }
+ }
+
+ // Check for properties that will cause an
+ // absolute url. Copy the other properties over.
+ if (
+ isset($hostOptions['_scheme']) ||
+ isset($hostOptions['_port']) ||
+ isset($hostOptions['_host'])
+ ) {
+ $hostOptions += $context;
+
+ if (
+ $hostOptions['_scheme'] &&
+ getservbyname($hostOptions['_scheme'], 'tcp') === $hostOptions['_port']
+ ) {
+ unset($hostOptions['_port']);
+ }
+ }
+
+ // If no base is set, copy one in.
+ if (!isset($hostOptions['_base']) && isset($context['_base'])) {
+ $hostOptions['_base'] = $context['_base'];
+ }
+
+ $query = !empty($url['?']) ? (array)$url['?'] : [];
+ unset($url['_host'], $url['_scheme'], $url['_port'], $url['_base'], $url['?']);
+
+ // Move extension into the hostOptions so its not part of
+ // reverse matches.
+ if (isset($url['_ext'])) {
+ $hostOptions['_ext'] = $url['_ext'];
+ unset($url['_ext']);
+ }
+
+ // Check the method first as it is special.
+ if (!$this->_matchMethod($url)) {
+ return null;
+ }
+ unset($url['_method'], $url['[method]'], $defaults['_method']);
+
+ // Missing defaults is a fail.
+ if (array_diff_key($defaults, $url) !== []) {
+ return null;
+ }
+
+ // Defaults with different values are a fail.
+ if (array_intersect_key($url, $defaults) != $defaults) {
+ return null;
+ }
+
+ // If this route uses pass option, and the passed elements are
+ // not set, rekey elements.
+ if (isset($this->options['pass'])) {
+ foreach ($this->options['pass'] as $i => $name) {
+ if (isset($url[$i]) && !isset($url[$name])) {
+ $url[$name] = $url[$i];
+ unset($url[$i]);
+ }
+ }
+ }
+
+ // check that all the key names are in the url
+ $keyNames = array_flip($this->keys);
+ if (array_intersect_key($keyNames, $url) !== $keyNames) {
+ return null;
+ }
+
+ $pass = [];
+ foreach ($url as $key => $value) {
+ // If the key is a routed key, it's not different yet.
+ if (array_key_exists($key, $keyNames)) {
+ continue;
+ }
+
+ // pull out passed args
+ $numeric = is_numeric($key);
+ if ($numeric && isset($defaults[$key]) && $defaults[$key] === $value) {
+ continue;
+ }
+ if ($numeric) {
+ $pass[] = $value;
+ unset($url[$key]);
+ continue;
+ }
+ }
+
+ // if not a greedy route, no extra params are allowed.
+ if (!$this->_greedy && !empty($pass)) {
+ return null;
+ }
+
+ // check patterns for routed params
+ if (!empty($this->options)) {
+ foreach ($this->options as $key => $pattern) {
+ if (isset($url[$key]) && !preg_match('#^' . $pattern . '$#u', (string)$url[$key])) {
+ return null;
+ }
+ }
+ }
+ $url += $hostOptions;
+
+ // Ensure controller/action keys are not null.
+ if (
+ (isset($keyNames['controller']) && !isset($url['controller'])) ||
+ (isset($keyNames['action']) && !isset($url['action']))
+ ) {
+ return null;
+ }
+
+ return $this->_writeUrl($url, $pass, $query);
+ }
+
+ /**
+ * Check whether or not the URL's HTTP method matches.
+ *
+ * @param array $url The array for the URL being generated.
+ * @return bool
+ */
+ protected function _matchMethod(array $url): bool
+ {
+ if (empty($this->defaults['_method'])) {
+ return true;
+ }
+ if (empty($url['_method'])) {
+ $url['_method'] = 'GET';
+ }
+ $defaults = (array)$this->defaults['_method'];
+ $methods = (array)$this->normalizeAndValidateMethods($url['_method']);
+ foreach ($methods as $value) {
+ if (in_array($value, $defaults, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Converts a matching route array into a URL string.
+ *
+ * Composes the string URL using the template
+ * used to create the route.
+ *
+ * @param array $params The params to convert to a string url
+ * @param array $pass The additional passed arguments
+ * @param array $query An array of parameters
+ * @return string Composed route string.
+ */
+ protected function _writeUrl(array $params, array $pass = [], array $query = []): string
+ {
+ $pass = implode('/', array_map('rawurlencode', $pass));
+ $out = $this->template;
+
+ $search = $replace = [];
+ foreach ($this->keys as $key) {
+ if (!array_key_exists($key, $params)) {
+ throw new InvalidArgumentException("Missing required route key `{$key}`");
+ }
+ $string = $params[$key];
+ if ($this->braceKeys) {
+ $search[] = "{{$key}}";
+ } else {
+ $search[] = ':' . $key;
+ }
+ $replace[] = $string;
+ }
+
+ if (strpos($this->template, '**') !== false) {
+ array_push($search, '**', '%2F');
+ array_push($replace, $pass, '/');
+ } elseif (strpos($this->template, '*') !== false) {
+ $search[] = '*';
+ $replace[] = $pass;
+ }
+ $out = str_replace($search, $replace, $out);
+
+ // add base url if applicable.
+ if (isset($params['_base'])) {
+ $out = $params['_base'] . $out;
+ unset($params['_base']);
+ }
+
+ $out = str_replace('//', '/', $out);
+ if (
+ isset($params['_scheme']) ||
+ isset($params['_host']) ||
+ isset($params['_port'])
+ ) {
+ $host = $params['_host'];
+
+ // append the port & scheme if they exists.
+ if (isset($params['_port'])) {
+ $host .= ':' . $params['_port'];
+ }
+ $scheme = $params['_scheme'] ?? 'http';
+ $out = "{$scheme}://{$host}{$out}";
+ }
+ if (!empty($params['_ext']) || !empty($query)) {
+ $out = rtrim($out, '/');
+ }
+ if (!empty($params['_ext'])) {
+ $out .= '.' . $params['_ext'];
+ }
+ if (!empty($query)) {
+ $out .= rtrim('?' . http_build_query($query), '?');
+ }
+
+ return $out;
+ }
+
+ /**
+ * Get the static path portion for this route.
+ *
+ * @return string
+ */
+ public function staticPath(): string
+ {
+ $routeKey = strpos($this->template, ':');
+ if ($routeKey !== false) {
+ return substr($this->template, 0, $routeKey);
+ }
+ $routeKey = strpos($this->template, '{');
+ if ($routeKey !== false && strpos($this->template, '}') !== false) {
+ return substr($this->template, 0, $routeKey);
+ }
+ $star = strpos($this->template, '*');
+ if ($star !== false) {
+ $path = rtrim(substr($this->template, 0, $star), '/');
+
+ return $path === '' ? '/' : $path;
+ }
+
+ return $this->template;
+ }
+
+ /**
+ * Set the names of the middleware that should be applied to this route.
+ *
+ * @param array $middleware The list of middleware names to apply to this route.
+ * Middleware names will not be checked until the route is matched.
+ * @return $this
+ */
+ public function setMiddleware(array $middleware)
+ {
+ $this->middleware = $middleware;
+
+ return $this;
+ }
+
+ /**
+ * Get the names of the middleware that should be applied to this route.
+ *
+ * @return array
+ */
+ public function getMiddleware(): array
+ {
+ return $this->middleware;
+ }
+
+ /**
+ * Set state magic method to support var_export
+ *
+ * This method helps for applications that want to implement
+ * router caching.
+ *
+ * @param array $fields Key/Value of object attributes
+ * @return static A new instance of the route
+ */
+ public static function __set_state(array $fields)
+ {
+ $class = static::class;
+ $obj = new $class('');
+ foreach ($fields as $field => $value) {
+ $obj->$field = $value;
+ }
+
+ return $obj;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/RouteBuilder.php b/app/vendor/cakephp/cakephp/src/Routing/RouteBuilder.php
new file mode 100644
index 000000000..dc9d63a9f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/RouteBuilder.php
@@ -0,0 +1,1034 @@
+ controller action map.
+ *
+ * @var array
+ */
+ protected static $_resourceMap = [
+ 'index' => ['action' => 'index', 'method' => 'GET', 'path' => ''],
+ 'create' => ['action' => 'add', 'method' => 'POST', 'path' => ''],
+ 'view' => ['action' => 'view', 'method' => 'GET', 'path' => ':id'],
+ 'update' => ['action' => 'edit', 'method' => ['PUT', 'PATCH'], 'path' => ':id'],
+ 'delete' => ['action' => 'delete', 'method' => 'DELETE', 'path' => ':id'],
+ ];
+
+ /**
+ * Default route class to use if none is provided in connect() options.
+ *
+ * @var string
+ */
+ protected $_routeClass = Route::class;
+
+ /**
+ * The extensions that should be set into the routes connected.
+ *
+ * @var string[]
+ */
+ protected $_extensions = [];
+
+ /**
+ * The path prefix scope that this collection uses.
+ *
+ * @var string
+ */
+ protected $_path;
+
+ /**
+ * The scope parameters if there are any.
+ *
+ * @var array
+ */
+ protected $_params;
+
+ /**
+ * Name prefix for connected routes.
+ *
+ * @var string
+ */
+ protected $_namePrefix = '';
+
+ /**
+ * The route collection routes should be added to.
+ *
+ * @var \Cake\Routing\RouteCollection
+ */
+ protected $_collection;
+
+ /**
+ * The list of middleware that routes in this builder get
+ * added during construction.
+ *
+ * @var string[]
+ */
+ protected $middleware = [];
+
+ /**
+ * Constructor
+ *
+ * ### Options
+ *
+ * - `routeClass` - The default route class to use when adding routes.
+ * - `extensions` - The extensions to connect when adding routes.
+ * - `namePrefix` - The prefix to prepend to all route names.
+ * - `middleware` - The names of the middleware routes should have applied.
+ *
+ * @param \Cake\Routing\RouteCollection $collection The route collection to append routes into.
+ * @param string $path The path prefix the scope is for.
+ * @param array $params The scope's routing parameters.
+ * @param array $options Options list.
+ */
+ public function __construct(RouteCollection $collection, string $path, array $params = [], array $options = [])
+ {
+ $this->_collection = $collection;
+ $this->_path = $path;
+ $this->_params = $params;
+ if (isset($options['routeClass'])) {
+ $this->_routeClass = $options['routeClass'];
+ }
+ if (isset($options['extensions'])) {
+ $this->_extensions = $options['extensions'];
+ }
+ if (isset($options['namePrefix'])) {
+ $this->_namePrefix = $options['namePrefix'];
+ }
+ if (isset($options['middleware'])) {
+ $this->middleware = (array)$options['middleware'];
+ }
+ }
+
+ /**
+ * Set default route class.
+ *
+ * @param string $routeClass Class name.
+ * @return $this
+ */
+ public function setRouteClass(string $routeClass)
+ {
+ $this->_routeClass = $routeClass;
+
+ return $this;
+ }
+
+ /**
+ * Get default route class.
+ *
+ * @return string
+ */
+ public function getRouteClass(): string
+ {
+ return $this->_routeClass;
+ }
+
+ /**
+ * Set the extensions in this route builder's scope.
+ *
+ * Future routes connected in through this builder will have the connected
+ * extensions applied. However, setting extensions does not modify existing routes.
+ *
+ * @param string|string[] $extensions The extensions to set.
+ * @return $this
+ */
+ public function setExtensions($extensions)
+ {
+ $this->_extensions = (array)$extensions;
+
+ return $this;
+ }
+
+ /**
+ * Get the extensions in this route builder's scope.
+ *
+ * @return string[]
+ */
+ public function getExtensions(): array
+ {
+ return $this->_extensions;
+ }
+
+ /**
+ * Add additional extensions to what is already in current scope
+ *
+ * @param string|string[] $extensions One or more extensions to add
+ * @return $this
+ */
+ public function addExtensions($extensions)
+ {
+ $extensions = array_merge($this->_extensions, (array)$extensions);
+ $this->_extensions = array_unique($extensions);
+
+ return $this;
+ }
+
+ /**
+ * Get the path this scope is for.
+ *
+ * @return string
+ */
+ public function path(): string
+ {
+ $routeKey = strpos($this->_path, ':');
+ if ($routeKey !== false) {
+ return substr($this->_path, 0, $routeKey);
+ }
+
+ return $this->_path;
+ }
+
+ /**
+ * Get the parameter names/values for this scope.
+ *
+ * @return array
+ */
+ public function params(): array
+ {
+ return $this->_params;
+ }
+
+ /**
+ * Checks if there is already a route with a given name.
+ *
+ * @param string $name Name.
+ * @return bool
+ */
+ public function nameExists(string $name): bool
+ {
+ return array_key_exists($name, $this->_collection->named());
+ }
+
+ /**
+ * Get/set the name prefix for this scope.
+ *
+ * Modifying the name prefix will only change the prefix
+ * used for routes connected after the prefix is changed.
+ *
+ * @param string|null $value Either the value to set or null.
+ * @return string
+ */
+ public function namePrefix(?string $value = null): string
+ {
+ if ($value !== null) {
+ $this->_namePrefix = $value;
+ }
+
+ return $this->_namePrefix;
+ }
+
+ /**
+ * Generate REST resource routes for the given controller(s).
+ *
+ * A quick way to generate a default routes to a set of REST resources (controller(s)).
+ *
+ * ### Usage
+ *
+ * Connect resource routes for an app controller:
+ *
+ * ```
+ * $routes->resources('Posts');
+ * ```
+ *
+ * Connect resource routes for the Comments controller in the
+ * Comments plugin:
+ *
+ * ```
+ * Router::plugin('Comments', function ($routes) {
+ * $routes->resources('Comments');
+ * });
+ * ```
+ *
+ * Plugins will create lowercase dasherized resource routes. e.g
+ * `/comments/comments`
+ *
+ * Connect resource routes for the Articles controller in the
+ * Admin prefix:
+ *
+ * ```
+ * Router::prefix('Admin', function ($routes) {
+ * $routes->resources('Articles');
+ * });
+ * ```
+ *
+ * Prefixes will create lowercase dasherized resource routes. e.g
+ * `/admin/posts`
+ *
+ * You can create nested resources by passing a callback in:
+ *
+ * ```
+ * $routes->resources('Articles', function ($routes) {
+ * $routes->resources('Comments');
+ * });
+ * ```
+ *
+ * The above would generate both resource routes for `/articles`, and `/articles/:article_id/comments`.
+ * You can use the `map` option to connect additional resource methods:
+ *
+ * ```
+ * $routes->resources('Articles', [
+ * 'map' => ['deleteAll' => ['action' => 'deleteAll', 'method' => 'DELETE']]
+ * ]);
+ * ```
+ *
+ * In addition to the default routes, this would also connect a route for `/articles/delete_all`.
+ * By default the path segment will match the key name. You can use the 'path' key inside the resource
+ * definition to customize the path name.
+ *
+ * You can use the `inflect` option to change how path segments are generated:
+ *
+ * ```
+ * $routes->resources('PaymentTypes', ['inflect' => 'underscore']);
+ * ```
+ *
+ * Will generate routes like `/payment-types` instead of `/payment_types`
+ *
+ * ### Options:
+ *
+ * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
+ * integer values and UUIDs.
+ * - 'inflect' - Choose the inflection method used on the resource name. Defaults to 'dasherize'.
+ * - 'only' - Only connect the specific list of actions.
+ * - 'actions' - Override the method names used for connecting actions.
+ * - 'map' - Additional resource routes that should be connected. If you define 'only' and 'map',
+ * make sure that your mapped methods are also in the 'only' list.
+ * - 'prefix' - Define a routing prefix for the resource controller. If the current scope
+ * defines a prefix, this prefix will be appended to it.
+ * - 'connectOptions' - Custom options for connecting the routes.
+ * - 'path' - Change the path so it doesn't match the resource name. E.g ArticlesController
+ * is available at `/posts`
+ *
+ * @param string $name A controller name to connect resource routes for.
+ * @param array|callable $options Options to use when generating REST routes, or a callback.
+ * @param callable|null $callback An optional callback to be executed in a nested scope. Nested
+ * scopes inherit the existing path and 'id' parameter.
+ * @return $this
+ */
+ public function resources(string $name, $options = [], $callback = null)
+ {
+ if (!is_array($options)) {
+ $callback = $options;
+ $options = [];
+ }
+ $options += [
+ 'connectOptions' => [],
+ 'inflect' => 'dasherize',
+ 'id' => static::ID . '|' . static::UUID,
+ 'only' => [],
+ 'actions' => [],
+ 'map' => [],
+ 'prefix' => null,
+ 'path' => null,
+ ];
+
+ foreach ($options['map'] as $k => $mapped) {
+ $options['map'][$k] += ['method' => 'GET', 'path' => $k, 'action' => ''];
+ }
+
+ $ext = null;
+ if (!empty($options['_ext'])) {
+ $ext = $options['_ext'];
+ }
+
+ $connectOptions = $options['connectOptions'];
+ if (empty($options['path'])) {
+ $method = $options['inflect'];
+ $options['path'] = Inflector::$method($name);
+ }
+ $resourceMap = array_merge(static::$_resourceMap, $options['map']);
+
+ $only = (array)$options['only'];
+ if (empty($only)) {
+ $only = array_keys($resourceMap);
+ }
+
+ $prefix = '';
+ if ($options['prefix']) {
+ $prefix = $options['prefix'];
+ }
+ if (isset($this->_params['prefix']) && $prefix) {
+ $prefix = $this->_params['prefix'] . '/' . $prefix;
+ }
+
+ foreach ($resourceMap as $method => $params) {
+ if (!in_array($method, $only, true)) {
+ continue;
+ }
+
+ $action = $params['action'];
+ if (isset($options['actions'][$method])) {
+ $action = $options['actions'][$method];
+ }
+
+ $url = '/' . implode('/', array_filter([$options['path'], $params['path']]));
+ $params = [
+ 'controller' => $name,
+ 'action' => $action,
+ '_method' => $params['method'],
+ ];
+ if ($prefix) {
+ $params['prefix'] = $prefix;
+ }
+ $routeOptions = $connectOptions + [
+ 'id' => $options['id'],
+ 'pass' => ['id'],
+ '_ext' => $ext,
+ ];
+ $this->connect($url, $params, $routeOptions);
+ }
+
+ if ($callback !== null) {
+ $idName = Inflector::singularize(Inflector::underscore($name)) . '_id';
+ $path = '/' . $options['path'] . '/:' . $idName;
+ $this->scope($path, [], $callback);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Create a route that only responds to GET requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function get(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('GET', $template, $target, $name);
+ }
+
+ /**
+ * Create a route that only responds to POST requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function post(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('POST', $template, $target, $name);
+ }
+
+ /**
+ * Create a route that only responds to PUT requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function put(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('PUT', $template, $target, $name);
+ }
+
+ /**
+ * Create a route that only responds to PATCH requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function patch(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('PATCH', $template, $target, $name);
+ }
+
+ /**
+ * Create a route that only responds to DELETE requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function delete(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('DELETE', $template, $target, $name);
+ }
+
+ /**
+ * Create a route that only responds to HEAD requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function head(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('HEAD', $template, $target, $name);
+ }
+
+ /**
+ * Create a route that only responds to OPTIONS requests.
+ *
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ public function options(string $template, $target, ?string $name = null): Route
+ {
+ return $this->_methodRoute('OPTIONS', $template, $target, $name);
+ }
+
+ /**
+ * Helper to create routes that only respond to a single HTTP method.
+ *
+ * @param string $method The HTTP method name to match.
+ * @param string $template The URL template to use.
+ * @param array|string $target An array describing the target route parameters. These parameters
+ * should indicate the plugin, prefix, controller, and action that this route points to.
+ * @param string|null $name The name of the route.
+ * @return \Cake\Routing\Route\Route
+ */
+ protected function _methodRoute(string $method, string $template, $target, ?string $name): Route
+ {
+ if ($name !== null) {
+ $name = $this->_namePrefix . $name;
+ }
+ $options = [
+ '_name' => $name,
+ '_ext' => $this->_extensions,
+ '_middleware' => $this->middleware,
+ 'routeClass' => $this->_routeClass,
+ ];
+
+ $target = $this->parseDefaults($target);
+ $target['_method'] = $method;
+
+ $route = $this->_makeRoute($template, $target, $options);
+ $this->_collection->add($route, $options);
+
+ return $route;
+ }
+
+ /**
+ * Load routes from a plugin.
+ *
+ * The routes file will have a local variable named `$routes` made available which contains
+ * the current RouteBuilder instance.
+ *
+ * @param string $name The plugin name
+ * @return $this
+ * @throws \Cake\Core\Exception\MissingPluginException When the plugin has not been loaded.
+ * @throws \InvalidArgumentException When the plugin does not have a routes file.
+ */
+ public function loadPlugin(string $name)
+ {
+ $plugins = Plugin::getCollection();
+ if (!$plugins->has($name)) {
+ throw new MissingPluginException(['plugin' => $name]);
+ }
+ $plugin = $plugins->get($name);
+ $plugin->routes($this);
+
+ // Disable the routes hook to prevent duplicate route issues.
+ $plugin->disable('routes');
+
+ return $this;
+ }
+
+ /**
+ * Connects a new Route.
+ *
+ * Routes are a way of connecting request URLs to objects in your application.
+ * At their core routes are a set or regular expressions that are used to
+ * match requests to destinations.
+ *
+ * Examples:
+ *
+ * ```
+ * $routes->connect('/{controller}/{action}/*');
+ * ```
+ *
+ * The first parameter will be used as a controller name while the second is
+ * used as the action name. The '/*' syntax makes this route greedy in that
+ * it will match requests like `/posts/index` as well as requests
+ * like `/posts/edit/1/foo/bar`.
+ *
+ * ```
+ * $routes->connect('/home-page', ['controller' => 'Pages', 'action' => 'display', 'home']);
+ * ```
+ *
+ * The above shows the use of route parameter defaults. And providing routing
+ * parameters for a static route.
+ *
+ * ```
+ * $routes->connect(
+ * '/{lang}/{controller}/{action}/{id}',
+ * [],
+ * ['id' => '[0-9]+', 'lang' => '[a-z]{3}']
+ * );
+ * ```
+ *
+ * Shows connecting a route with custom route parameters as well as
+ * providing patterns for those parameters. Patterns for routing parameters
+ * do not need capturing groups, as one will be added for each route params.
+ *
+ * $options offers several 'special' keys that have special meaning
+ * in the $options array.
+ *
+ * - `routeClass` is used to extend and change how individual routes parse requests
+ * and handle reverse routing, via a custom routing class.
+ * Ex. `'routeClass' => 'SlugRoute'`
+ * - `pass` is used to define which of the routed parameters should be shifted
+ * into the pass array. Adding a parameter to pass will remove it from the
+ * regular route array. Ex. `'pass' => ['slug']`.
+ * - `persist` is used to define which route parameters should be automatically
+ * included when generating new URLs. You can override persistent parameters
+ * by redefining them in a URL or remove them by setting the parameter to `false`.
+ * Ex. `'persist' => ['lang']`
+ * - `multibytePattern` Set to true to enable multibyte pattern support in route
+ * parameter patterns.
+ * - `_name` is used to define a specific name for routes. This can be used to optimize
+ * reverse routing lookups. If undefined a name will be generated for each
+ * connected route.
+ * - `_ext` is an array of filename extensions that will be parsed out of the url if present.
+ * See {@link \Cake\Routing\RouteCollection::setExtensions()}.
+ * - `_method` Only match requests with specific HTTP verbs.
+ *
+ * Example of using the `_method` condition:
+ *
+ * ```
+ * $routes->connect('/tasks', ['controller' => 'Tasks', 'action' => 'index', '_method' => 'GET']);
+ * ```
+ *
+ * The above route will only be matched for GET requests. POST requests will fail to match this route.
+ *
+ * @param string|\Cake\Routing\Route\Route $route A string describing the template of the route
+ * @param array|string $defaults An array describing the default route parameters.
+ * These parameters will be used by default and can supply routing parameters that are not dynamic. See above.
+ * @param array $options An array matching the named elements in the route to regular expressions which that
+ * element should match. Also contains additional parameters such as which routed parameters should be
+ * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
+ * custom routing class.
+ * @return \Cake\Routing\Route\Route
+ * @throws \InvalidArgumentException
+ * @throws \BadMethodCallException
+ */
+ public function connect($route, $defaults = [], array $options = []): Route
+ {
+ $defaults = $this->parseDefaults($defaults);
+ if (empty($options['_ext'])) {
+ $options['_ext'] = $this->_extensions;
+ }
+ if (empty($options['routeClass'])) {
+ $options['routeClass'] = $this->_routeClass;
+ }
+ if (isset($options['_name']) && $this->_namePrefix) {
+ $options['_name'] = $this->_namePrefix . $options['_name'];
+ }
+ if (empty($options['_middleware'])) {
+ $options['_middleware'] = $this->middleware;
+ }
+
+ $route = $this->_makeRoute($route, $defaults, $options);
+ $this->_collection->add($route, $options);
+
+ return $route;
+ }
+
+ /**
+ * Parse the defaults if they're a string
+ *
+ * @param string|array $defaults Defaults array from the connect() method.
+ * @return array
+ */
+ protected function parseDefaults($defaults): array
+ {
+ if (!is_string($defaults)) {
+ return $defaults;
+ }
+
+ return Router::parseRoutePath($defaults);
+ }
+
+ /**
+ * Create a route object, or return the provided object.
+ *
+ * @param string|\Cake\Routing\Route\Route $route The route template or route object.
+ * @param array $defaults Default parameters.
+ * @param array $options Additional options parameters.
+ * @return \Cake\Routing\Route\Route
+ * @throws \InvalidArgumentException when route class or route object is invalid.
+ * @throws \BadMethodCallException when the route to make conflicts with the current scope
+ */
+ protected function _makeRoute($route, $defaults, $options): Route
+ {
+ if (is_string($route)) {
+ $routeClass = App::className($options['routeClass'], 'Routing/Route');
+ if ($routeClass === null) {
+ throw new InvalidArgumentException(sprintf(
+ 'Cannot find route class %s',
+ $options['routeClass']
+ ));
+ }
+
+ $route = str_replace('//', '/', $this->_path . $route);
+ if ($route !== '/') {
+ $route = rtrim($route, '/');
+ }
+
+ foreach ($this->_params as $param => $val) {
+ if (isset($defaults[$param]) && $param !== 'prefix' && $defaults[$param] !== $val) {
+ $msg = 'You cannot define routes that conflict with the scope. ' .
+ 'Scope had %s = %s, while route had %s = %s';
+ throw new BadMethodCallException(sprintf(
+ $msg,
+ $param,
+ $val,
+ $param,
+ $defaults[$param]
+ ));
+ }
+ }
+ $defaults += $this->_params + ['plugin' => null];
+ if (!isset($defaults['action']) && !isset($options['action'])) {
+ $defaults['action'] = 'index';
+ }
+
+ $route = new $routeClass($route, $defaults, $options);
+ }
+
+ if ($route instanceof Route) {
+ return $route;
+ }
+ throw new InvalidArgumentException(
+ 'Route class not found, or route class is not a subclass of Cake\Routing\Route\Route'
+ );
+ }
+
+ /**
+ * Connects a new redirection Route in the router.
+ *
+ * Redirection routes are different from normal routes as they perform an actual
+ * header redirection if a match is found. The redirection can occur within your
+ * application or redirect to an outside location.
+ *
+ * Examples:
+ *
+ * ```
+ * $routes->redirect('/home/*', ['controller' => 'Posts', 'action' => 'view']);
+ * ```
+ *
+ * Redirects /home/* to /posts/view and passes the parameters to /posts/view. Using an array as the
+ * redirect destination allows you to use other routes to define where a URL string should be redirected to.
+ *
+ * ```
+ * $routes->redirect('/posts/*', 'http://google.com', ['status' => 302]);
+ * ```
+ *
+ * Redirects /posts/* to http://google.com with a HTTP status of 302
+ *
+ * ### Options:
+ *
+ * - `status` Sets the HTTP status (default 301)
+ * - `persist` Passes the params to the redirected route, if it can. This is useful with greedy routes,
+ * routes that end in `*` are greedy. As you can remap URLs and not lose any passed args.
+ *
+ * @param string $route A string describing the template of the route
+ * @param array|string $url A URL to redirect to. Can be a string or a Cake array-based URL
+ * @param array $options An array matching the named elements in the route to regular expressions which that
+ * element should match. Also contains additional parameters such as which routed parameters should be
+ * shifted into the passed arguments. As well as supplying patterns for routing parameters.
+ * @return \Cake\Routing\Route\Route|\Cake\Routing\Route\RedirectRoute
+ */
+ public function redirect(string $route, $url, array $options = []): Route
+ {
+ if (!isset($options['routeClass'])) {
+ $options['routeClass'] = RedirectRoute::class;
+ }
+ if (is_string($url)) {
+ $url = ['redirect' => $url];
+ }
+
+ return $this->connect($route, $url, $options);
+ }
+
+ /**
+ * Add prefixed routes.
+ *
+ * This method creates a scoped route collection that includes
+ * relevant prefix information.
+ *
+ * The $name parameter is used to generate the routing parameter name.
+ * For example a path of `admin` would result in `'prefix' => 'admin'` being
+ * applied to all connected routes.
+ *
+ * You can re-open a prefix as many times as necessary, as well as nest prefixes.
+ * Nested prefixes will result in prefix values like `admin/api` which translates
+ * to the `Controller\Admin\Api\` namespace.
+ *
+ * If you need to have prefix with dots, eg: '/api/v1.0', use 'path' key
+ * for $params argument:
+ *
+ * ```
+ * $route->prefix('Api', function($route) {
+ * $route->prefix('V10', ['path' => '/v1.0'], function($route) {
+ * // Translates to `Controller\Api\V10\` namespace
+ * });
+ * });
+ * ```
+ *
+ * @param string $name The prefix name to use.
+ * @param array|callable $params An array of routing defaults to add to each connected route.
+ * If you have no parameters, this argument can be a callable.
+ * @param callable|null $callback The callback to invoke that builds the prefixed routes.
+ * @return $this
+ * @throws \InvalidArgumentException If a valid callback is not passed
+ */
+ public function prefix(string $name, $params = [], $callback = null)
+ {
+ if (!is_array($params)) {
+ $callback = $params;
+ $params = [];
+ }
+ $path = '/' . Inflector::dasherize($name);
+ $name = Inflector::camelize($name);
+ if (isset($params['path'])) {
+ $path = $params['path'];
+ unset($params['path']);
+ }
+ if (isset($this->_params['prefix'])) {
+ $name = $this->_params['prefix'] . '/' . $name;
+ }
+ $params = array_merge($params, ['prefix' => $name]);
+ $this->scope($path, $params, $callback);
+
+ return $this;
+ }
+
+ /**
+ * Add plugin routes.
+ *
+ * This method creates a new scoped route collection that includes
+ * relevant plugin information.
+ *
+ * The plugin name will be inflected to the underscore version to create
+ * the routing path. If you want a custom path name, use the `path` option.
+ *
+ * Routes connected in the scoped collection will have the correct path segment
+ * prepended, and have a matching plugin routing key set.
+ *
+ * ### Options
+ *
+ * - `path` The path prefix to use. Defaults to `Inflector::dasherize($name)`.
+ * - `_namePrefix` Set a prefix used for named routes. The prefix is prepended to the
+ * name of any route created in a scope callback.
+ *
+ * @param string $name The plugin name to build routes for
+ * @param array|callable $options Either the options to use, or a callback to build routes.
+ * @param callable|null $callback The callback to invoke that builds the plugin routes
+ * Only required when $options is defined.
+ * @return $this
+ */
+ public function plugin(string $name, $options = [], $callback = null)
+ {
+ if (!is_array($options)) {
+ $callback = $options;
+ $options = [];
+ }
+
+ $path = $options['path'] ?? '/' . Inflector::dasherize($name);
+ unset($options['path']);
+ $options = ['plugin' => $name] + $options;
+ $this->scope($path, $options, $callback);
+
+ return $this;
+ }
+
+ /**
+ * Create a new routing scope.
+ *
+ * Scopes created with this method will inherit the properties of the scope they are
+ * added to. This means that both the current path and parameters will be appended
+ * to the supplied parameters.
+ *
+ * ### Special Keys in $params
+ *
+ * - `_namePrefix` Set a prefix used for named routes. The prefix is prepended to the
+ * name of any route created in a scope callback.
+ *
+ * @param string $path The path to create a scope for.
+ * @param array|callable $params Either the parameters to add to routes, or a callback.
+ * @param callable|null $callback The callback to invoke that builds the plugin routes.
+ * Only required when $params is defined.
+ * @return $this
+ * @throws \InvalidArgumentException when there is no callable parameter.
+ */
+ public function scope(string $path, $params, $callback = null)
+ {
+ if (!is_array($params)) {
+ $callback = $params;
+ $params = [];
+ }
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException(sprintf(
+ 'Need a valid callable to connect routes. Got `%s` instead.',
+ getTypeName($callback)
+ ));
+ }
+
+ if ($this->_path !== '/') {
+ $path = $this->_path . $path;
+ }
+ $namePrefix = $this->_namePrefix;
+ if (isset($params['_namePrefix'])) {
+ $namePrefix .= $params['_namePrefix'];
+ }
+ unset($params['_namePrefix']);
+
+ $params += $this->_params;
+ $builder = new static($this->_collection, $path, $params, [
+ 'routeClass' => $this->_routeClass,
+ 'extensions' => $this->_extensions,
+ 'namePrefix' => $namePrefix,
+ 'middleware' => $this->middleware,
+ ]);
+ $callback($builder);
+
+ return $this;
+ }
+
+ /**
+ * Connect the `/{controller}` and `/{controller}/{action}/*` fallback routes.
+ *
+ * This is a shortcut method for connecting fallback routes in a given scope.
+ *
+ * @param string|null $routeClass the route class to use, uses the default routeClass
+ * if not specified
+ * @return $this
+ */
+ public function fallbacks(?string $routeClass = null)
+ {
+ $routeClass = $routeClass ?: $this->_routeClass;
+ $this->connect('/{controller}', ['action' => 'index'], compact('routeClass'));
+ $this->connect('/{controller}/{action}/*', [], compact('routeClass'));
+
+ return $this;
+ }
+
+ /**
+ * Register a middleware with the RouteCollection.
+ *
+ * Once middleware has been registered, it can be applied to the current routing
+ * scope or any child scopes that share the same RouteCollection.
+ *
+ * @param string $name The name of the middleware. Used when applying middleware to a scope.
+ * @param string|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware to register.
+ * @return $this
+ * @see \Cake\Routing\RouteCollection
+ */
+ public function registerMiddleware(string $name, $middleware)
+ {
+ $this->_collection->registerMiddleware($name, $middleware);
+
+ return $this;
+ }
+
+ /**
+ * Apply a middleware to the current route scope.
+ *
+ * Requires middleware to be registered via `registerMiddleware()`
+ *
+ * @param string ...$names The names of the middleware to apply to the current scope.
+ * @return $this
+ * @throws \RuntimeException
+ * @see \Cake\Routing\RouteCollection::addMiddlewareToScope()
+ */
+ public function applyMiddleware(string ...$names)
+ {
+ foreach ($names as $name) {
+ if (!$this->_collection->middlewareExists($name)) {
+ $message = "Cannot apply '$name' middleware or middleware group. " .
+ 'Use registerMiddleware() to register middleware.';
+ throw new RuntimeException($message);
+ }
+ }
+ $this->middleware = array_unique(array_merge($this->middleware, $names));
+
+ return $this;
+ }
+
+ /**
+ * Get the middleware that this builder will apply to routes.
+ *
+ * @return array
+ */
+ public function getMiddleware(): array
+ {
+ return $this->middleware;
+ }
+
+ /**
+ * Apply a set of middleware to a group
+ *
+ * @param string $name Name of the middleware group
+ * @param string[] $middlewareNames Names of the middleware
+ * @return $this
+ */
+ public function middlewareGroup(string $name, array $middlewareNames)
+ {
+ $this->_collection->middlewareGroup($name, $middlewareNames);
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php b/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php
new file mode 100644
index 000000000..0cb87e739
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/RouteCollection.php
@@ -0,0 +1,494 @@
+>
+ */
+ protected $_paths = [];
+
+ /**
+ * A map of middleware names and the related objects.
+ *
+ * @var array
+ */
+ protected $_middleware = [];
+
+ /**
+ * A map of middleware group names and the related middleware names.
+ *
+ * @var array
+ */
+ protected $_middlewareGroups = [];
+
+ /**
+ * Route extensions
+ *
+ * @var string[]
+ */
+ protected $_extensions = [];
+
+ /**
+ * Add a route to the collection.
+ *
+ * @param \Cake\Routing\Route\Route $route The route object to add.
+ * @param array $options Additional options for the route. Primarily for the
+ * `_name` option, which enables named routes.
+ * @return void
+ */
+ public function add(Route $route, array $options = []): void
+ {
+ // Explicit names
+ if (isset($options['_name'])) {
+ if (isset($this->_named[$options['_name']])) {
+ $matched = $this->_named[$options['_name']];
+ throw new DuplicateNamedRouteException([
+ 'name' => $options['_name'],
+ 'url' => $matched->template,
+ 'duplicate' => $matched,
+ ]);
+ }
+ $this->_named[$options['_name']] = $route;
+ }
+
+ // Generated names.
+ $name = $route->getName();
+ if (!isset($this->_routeTable[$name])) {
+ $this->_routeTable[$name] = [];
+ }
+ $this->_routeTable[$name][] = $route;
+
+ // Index path prefixes (for parsing)
+ $path = $route->staticPath();
+ $this->_paths[$path][] = $route;
+
+ $extensions = $route->getExtensions();
+ if (count($extensions) > 0) {
+ $this->setExtensions($extensions);
+ }
+ }
+
+ /**
+ * Takes the URL string and iterates the routes until one is able to parse the route.
+ *
+ * @param string $url URL to parse.
+ * @param string $method The HTTP method to use.
+ * @return array An array of request parameters parsed from the URL.
+ * @throws \Cake\Routing\Exception\MissingRouteException When a URL has no matching route.
+ */
+ public function parse(string $url, string $method = ''): array
+ {
+ $decoded = urldecode($url);
+
+ // Sort path segments matching longest paths first.
+ krsort($this->_paths);
+
+ foreach ($this->_paths as $path => $routes) {
+ if (strpos($decoded, $path) !== 0) {
+ continue;
+ }
+
+ $queryParameters = [];
+ if (strpos($url, '?') !== false) {
+ [$url, $qs] = explode('?', $url, 2);
+ parse_str($qs, $queryParameters);
+ }
+
+ foreach ($routes as $route) {
+ $r = $route->parse($url, $method);
+ if ($r === null) {
+ continue;
+ }
+ if ($queryParameters) {
+ $r['?'] = $queryParameters;
+ }
+
+ return $r;
+ }
+ }
+
+ $exceptionProperties = ['url' => $url];
+ if ($method !== '') {
+ // Ensure that if the method is included, it is the first element of
+ // the array, to match the order that the strings are printed in the
+ // MissingRouteException error message, $_messageTemplateWithMethod.
+ $exceptionProperties = array_merge(['method' => $method], $exceptionProperties);
+ }
+ throw new MissingRouteException($exceptionProperties);
+ }
+
+ /**
+ * Takes the ServerRequestInterface, iterates the routes until one is able to parse the route.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to parse route data from.
+ * @return array An array of request parameters parsed from the URL.
+ * @throws \Cake\Routing\Exception\MissingRouteException When a URL has no matching route.
+ */
+ public function parseRequest(ServerRequestInterface $request): array
+ {
+ $uri = $request->getUri();
+ $urlPath = urldecode($uri->getPath());
+
+ // Sort path segments matching longest paths first.
+ krsort($this->_paths);
+
+ foreach ($this->_paths as $path => $routes) {
+ if (strpos($urlPath, $path) !== 0) {
+ continue;
+ }
+
+ foreach ($routes as $route) {
+ $r = $route->parseRequest($request);
+ if ($r === null) {
+ continue;
+ }
+ if ($uri->getQuery()) {
+ parse_str($uri->getQuery(), $queryParameters);
+ $r['?'] = $queryParameters;
+ }
+
+ return $r;
+ }
+ }
+ throw new MissingRouteException(['url' => $urlPath]);
+ }
+
+ /**
+ * Get the set of names from the $url. Accepts both older style array urls,
+ * and newer style urls containing '_name'
+ *
+ * @param array $url The url to match.
+ * @return string[] The set of names of the url
+ */
+ protected function _getNames(array $url): array
+ {
+ $plugin = false;
+ if (isset($url['plugin']) && $url['plugin'] !== false) {
+ $plugin = strtolower($url['plugin']);
+ }
+ $prefix = false;
+ if (isset($url['prefix']) && $url['prefix'] !== false) {
+ $prefix = strtolower($url['prefix']);
+ }
+ $controller = isset($url['controller']) ? strtolower($url['controller']) : null;
+ $action = strtolower($url['action']);
+
+ $names = [
+ "${controller}:${action}",
+ "${controller}:_action",
+ "_controller:${action}",
+ '_controller:_action',
+ ];
+
+ // No prefix, no plugin
+ if ($prefix === false && $plugin === false) {
+ return $names;
+ }
+
+ // Only a plugin
+ if ($prefix === false) {
+ return [
+ "${plugin}.${controller}:${action}",
+ "${plugin}.${controller}:_action",
+ "${plugin}._controller:${action}",
+ "${plugin}._controller:_action",
+ "_plugin.${controller}:${action}",
+ "_plugin.${controller}:_action",
+ "_plugin._controller:${action}",
+ '_plugin._controller:_action',
+ ];
+ }
+
+ // Only a prefix
+ if ($plugin === false) {
+ return [
+ "${prefix}:${controller}:${action}",
+ "${prefix}:${controller}:_action",
+ "${prefix}:_controller:${action}",
+ "${prefix}:_controller:_action",
+ "_prefix:${controller}:${action}",
+ "_prefix:${controller}:_action",
+ "_prefix:_controller:${action}",
+ '_prefix:_controller:_action',
+ ];
+ }
+
+ // Prefix and plugin has the most options
+ // as there are 4 factors.
+ return [
+ "${prefix}:${plugin}.${controller}:${action}",
+ "${prefix}:${plugin}.${controller}:_action",
+ "${prefix}:${plugin}._controller:${action}",
+ "${prefix}:${plugin}._controller:_action",
+ "${prefix}:_plugin.${controller}:${action}",
+ "${prefix}:_plugin.${controller}:_action",
+ "${prefix}:_plugin._controller:${action}",
+ "${prefix}:_plugin._controller:_action",
+ "_prefix:${plugin}.${controller}:${action}",
+ "_prefix:${plugin}.${controller}:_action",
+ "_prefix:${plugin}._controller:${action}",
+ "_prefix:${plugin}._controller:_action",
+ "_prefix:_plugin.${controller}:${action}",
+ "_prefix:_plugin.${controller}:_action",
+ "_prefix:_plugin._controller:${action}",
+ '_prefix:_plugin._controller:_action',
+ ];
+ }
+
+ /**
+ * Reverse route or match a $url array with the connected routes.
+ *
+ * Returns either the URL string generated by the route,
+ * or throws an exception on failure.
+ *
+ * @param array $url The URL to match.
+ * @param array $context The request context to use. Contains _base, _port,
+ * _host, _scheme and params keys.
+ * @return string The URL string on match.
+ * @throws \Cake\Routing\Exception\MissingRouteException When no route could be matched.
+ */
+ public function match(array $url, array $context): string
+ {
+ // Named routes support optimization.
+ if (isset($url['_name'])) {
+ $name = $url['_name'];
+ unset($url['_name']);
+ if (isset($this->_named[$name])) {
+ $route = $this->_named[$name];
+ $out = $route->match($url + $route->defaults, $context);
+ if ($out) {
+ return $out;
+ }
+ throw new MissingRouteException([
+ 'url' => $name,
+ 'context' => $context,
+ 'message' => "A named route was found for `{$name}`, but matching failed.",
+ ]);
+ }
+ throw new MissingRouteException(['url' => $name, 'context' => $context]);
+ }
+
+ foreach ($this->_getNames($url) as $name) {
+ if (empty($this->_routeTable[$name])) {
+ continue;
+ }
+ /** @var \Cake\Routing\Route\Route $route */
+ foreach ($this->_routeTable[$name] as $route) {
+ $match = $route->match($url, $context);
+ if ($match) {
+ return strlen($match) > 1 ? trim($match, '/') : $match;
+ }
+ }
+ }
+ throw new MissingRouteException(['url' => var_export($url, true), 'context' => $context]);
+ }
+
+ /**
+ * Get all the connected routes as a flat list.
+ *
+ * @return \Cake\Routing\Route\Route[]
+ */
+ public function routes(): array
+ {
+ krsort($this->_paths);
+
+ return array_reduce(
+ $this->_paths,
+ 'array_merge',
+ []
+ );
+ }
+
+ /**
+ * Get the connected named routes.
+ *
+ * @return \Cake\Routing\Route\Route[]
+ */
+ public function named(): array
+ {
+ return $this->_named;
+ }
+
+ /**
+ * Get the extensions that can be handled.
+ *
+ * @return string[] The valid extensions.
+ */
+ public function getExtensions(): array
+ {
+ return $this->_extensions;
+ }
+
+ /**
+ * Set the extensions that the route collection can handle.
+ *
+ * @param string[] $extensions The list of extensions to set.
+ * @param bool $merge Whether to merge with or override existing extensions.
+ * Defaults to `true`.
+ * @return $this
+ */
+ public function setExtensions(array $extensions, bool $merge = true)
+ {
+ if ($merge) {
+ $extensions = array_unique(array_merge(
+ $this->_extensions,
+ $extensions
+ ));
+ }
+ $this->_extensions = $extensions;
+
+ return $this;
+ }
+
+ /**
+ * Register a middleware with the RouteCollection.
+ *
+ * Once middleware has been registered, it can be applied to the current routing
+ * scope or any child scopes that share the same RouteCollection.
+ *
+ * @param string $name The name of the middleware. Used when applying middleware to a scope.
+ * @param string|\Closure|\Psr\Http\Server\MiddlewareInterface $middleware The middleware to register.
+ * @return $this
+ * @throws \RuntimeException
+ */
+ public function registerMiddleware(string $name, $middleware)
+ {
+ $this->_middleware[$name] = $middleware;
+
+ return $this;
+ }
+
+ /**
+ * Add middleware to a middleware group
+ *
+ * @param string $name Name of the middleware group
+ * @param string[] $middlewareNames Names of the middleware
+ * @return $this
+ * @throws \RuntimeException
+ */
+ public function middlewareGroup(string $name, array $middlewareNames)
+ {
+ if ($this->hasMiddleware($name)) {
+ $message = "Cannot add middleware group '$name'. A middleware by this name has already been registered.";
+ throw new RuntimeException($message);
+ }
+
+ foreach ($middlewareNames as $middlewareName) {
+ if (!$this->hasMiddleware($middlewareName)) {
+ $message = "Cannot add '$middlewareName' middleware to group '$name'. It has not been registered.";
+ throw new RuntimeException($message);
+ }
+ }
+
+ $this->_middlewareGroups[$name] = $middlewareNames;
+
+ return $this;
+ }
+
+ /**
+ * Check if the named middleware group has been created.
+ *
+ * @param string $name The name of the middleware group to check.
+ * @return bool
+ */
+ public function hasMiddlewareGroup(string $name): bool
+ {
+ return array_key_exists($name, $this->_middlewareGroups);
+ }
+
+ /**
+ * Check if the named middleware has been registered.
+ *
+ * @param string $name The name of the middleware to check.
+ * @return bool
+ */
+ public function hasMiddleware(string $name): bool
+ {
+ return isset($this->_middleware[$name]);
+ }
+
+ /**
+ * Check if the named middleware or middleware group has been registered.
+ *
+ * @param string $name The name of the middleware to check.
+ * @return bool
+ */
+ public function middlewareExists(string $name): bool
+ {
+ return $this->hasMiddleware($name) || $this->hasMiddlewareGroup($name);
+ }
+
+ /**
+ * Get an array of middleware given a list of names
+ *
+ * @param string[] $names The names of the middleware or groups to fetch
+ * @return array An array of middleware. If any of the passed names are groups,
+ * the groups middleware will be flattened into the returned list.
+ * @throws \RuntimeException when a requested middleware does not exist.
+ */
+ public function getMiddleware(array $names): array
+ {
+ $out = [];
+ foreach ($names as $name) {
+ if ($this->hasMiddlewareGroup($name)) {
+ $out = array_merge($out, $this->getMiddleware($this->_middlewareGroups[$name]));
+ continue;
+ }
+ if (!$this->hasMiddleware($name)) {
+ throw new RuntimeException(sprintf(
+ "The middleware named '%s' has not been registered. Use registerMiddleware() to define it.",
+ $name
+ ));
+ }
+ $out[] = $this->_middleware[$name];
+ }
+
+ return $out;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/Router.php b/app/vendor/cakephp/cakephp/src/Routing/Router.php
new file mode 100644
index 000000000..0c2743b6d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/Router.php
@@ -0,0 +1,997 @@
+ Router::ACTION,
+ 'Year' => Router::YEAR,
+ 'Month' => Router::MONTH,
+ 'Day' => Router::DAY,
+ 'ID' => Router::ID,
+ 'UUID' => Router::UUID,
+ ];
+
+ /**
+ * Maintains the request object reference.
+ *
+ * @var \Cake\Http\ServerRequest
+ */
+ protected static $_request;
+
+ /**
+ * Initial state is populated the first time reload() is called which is at the bottom
+ * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they
+ * have changed.
+ *
+ * @var array
+ */
+ protected static $_initialState = [];
+
+ /**
+ * The stack of URL filters to apply against routing URLs before passing the
+ * parameters to the route collection.
+ *
+ * @var callable[]
+ */
+ protected static $_urlFilters = [];
+
+ /**
+ * Default extensions defined with Router::extensions()
+ *
+ * @var string[]
+ */
+ protected static $_defaultExtensions = [];
+
+ /**
+ * Cache of parsed route paths
+ *
+ * @var array
+ */
+ protected static $_routePaths = [];
+
+ /**
+ * Get or set default route class.
+ *
+ * @param string|null $routeClass Class name.
+ * @return string|null
+ */
+ public static function defaultRouteClass(?string $routeClass = null): ?string
+ {
+ if ($routeClass === null) {
+ return static::$_defaultRouteClass;
+ }
+ static::$_defaultRouteClass = $routeClass;
+
+ return null;
+ }
+
+ /**
+ * Gets the named route patterns for use in config/routes.php
+ *
+ * @return array Named route elements
+ * @see \Cake\Routing\Router::$_namedExpressions
+ */
+ public static function getNamedExpressions(): array
+ {
+ return static::$_namedExpressions;
+ }
+
+ /**
+ * Connects a new Route in the router.
+ *
+ * Compatibility proxy to \Cake\Routing\RouteBuilder::connect() in the `/` scope.
+ *
+ * @param string|\Cake\Routing\Route\Route $route A string describing the template of the route
+ * @param array|string $defaults An array describing the default route parameters.
+ * These parameters will be used by default and can supply routing parameters that are not dynamic. See above.
+ * @param array $options An array matching the named elements in the route to regular expressions which that
+ * element should match. Also contains additional parameters such as which routed parameters should be
+ * shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
+ * custom routing class.
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException
+ * @see \Cake\Routing\RouteBuilder::connect()
+ * @see \Cake\Routing\Router::scope()
+ */
+ public static function connect($route, $defaults = [], $options = []): void
+ {
+ static::scope('/', function ($routes) use ($route, $defaults, $options): void {
+ /** @var \Cake\Routing\RouteBuilder $routes */
+ $routes->connect($route, $defaults, $options);
+ });
+ }
+
+ /**
+ * Get the routing parameters for the request is possible.
+ *
+ * @param \Cake\Http\ServerRequest $request The request to parse request data from.
+ * @return array Parsed elements from URL.
+ * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
+ */
+ public static function parseRequest(ServerRequest $request): array
+ {
+ return static::$_collection->parseRequest($request);
+ }
+
+ /**
+ * Set current request instance.
+ *
+ * @param \Cake\Http\ServerRequest $request request object.
+ * @return void
+ */
+ public static function setRequest(ServerRequest $request): void
+ {
+ static::$_request = $request;
+
+ static::$_requestContext['_base'] = $request->getAttribute('base');
+ static::$_requestContext['params'] = $request->getAttribute('params', []);
+
+ $uri = $request->getUri();
+ static::$_requestContext += [
+ '_scheme' => $uri->getScheme(),
+ '_host' => $uri->getHost(),
+ '_port' => $uri->getPort(),
+ ];
+ }
+
+ /**
+ * Get the current request object.
+ *
+ * @return \Cake\Http\ServerRequest|null
+ */
+ public static function getRequest(): ?ServerRequest
+ {
+ return static::$_request;
+ }
+
+ /**
+ * Reloads default Router settings. Resets all class variables and
+ * removes all connected routes.
+ *
+ * @return void
+ */
+ public static function reload(): void
+ {
+ if (empty(static::$_initialState)) {
+ static::$_collection = new RouteCollection();
+ static::$_initialState = get_class_vars(static::class);
+
+ return;
+ }
+ foreach (static::$_initialState as $key => $val) {
+ if ($key !== '_initialState') {
+ static::${$key} = $val;
+ }
+ }
+ static::$_collection = new RouteCollection();
+ }
+
+ /**
+ * Reset routes and related state.
+ *
+ * Similar to reload() except that this doesn't reset all global state,
+ * as that leads to incorrect behavior in some plugin test case scenarios.
+ *
+ * This method will reset:
+ *
+ * - routes
+ * - URL Filters
+ * - the initialized property
+ *
+ * Extensions and default route classes will not be modified
+ *
+ * @internal
+ * @return void
+ */
+ public static function resetRoutes(): void
+ {
+ static::$_collection = new RouteCollection();
+ static::$_urlFilters = [];
+ }
+
+ /**
+ * Add a URL filter to Router.
+ *
+ * URL filter functions are applied to every array $url provided to
+ * Router::url() before the URLs are sent to the route collection.
+ *
+ * Callback functions should expect the following parameters:
+ *
+ * - `$params` The URL params being processed.
+ * - `$request` The current request.
+ *
+ * The URL filter function should *always* return the params even if unmodified.
+ *
+ * ### Usage
+ *
+ * URL filters allow you to easily implement features like persistent parameters.
+ *
+ * ```
+ * Router::addUrlFilter(function ($params, $request) {
+ * if ($request->getParam('lang') && !isset($params['lang'])) {
+ * $params['lang'] = $request->getParam('lang');
+ * }
+ * return $params;
+ * });
+ * ```
+ *
+ * @param callable $function The function to add
+ * @return void
+ */
+ public static function addUrlFilter(callable $function): void
+ {
+ static::$_urlFilters[] = $function;
+ }
+
+ /**
+ * Applies all the connected URL filters to the URL.
+ *
+ * @param array $url The URL array being modified.
+ * @return array The modified URL.
+ * @see \Cake\Routing\Router::url()
+ * @see \Cake\Routing\Router::addUrlFilter()
+ */
+ protected static function _applyUrlFilters(array $url): array
+ {
+ $request = static::getRequest();
+ foreach (static::$_urlFilters as $filter) {
+ try {
+ $url = $filter($url, $request);
+ } catch (Throwable $e) {
+ if (is_array($filter)) {
+ $ref = new ReflectionMethod($filter[0], $filter[1]);
+ } else {
+ /** @psalm-var \Closure|callable-string $filter */
+ $ref = new ReflectionFunction($filter);
+ }
+ $message = sprintf(
+ 'URL filter defined in %s on line %s could not be applied. The filter failed with: %s',
+ $ref->getFileName(),
+ $ref->getStartLine(),
+ $e->getMessage()
+ );
+ throw new RuntimeException($message, (int)$e->getCode(), $e);
+ }
+ }
+
+ return $url;
+ }
+
+ /**
+ * Finds URL for specified action.
+ *
+ * Returns a URL pointing to a combination of controller and action.
+ *
+ * ### Usage
+ *
+ * - `Router::url('/posts/edit/1');` Returns the string with the base dir prepended.
+ * This usage does not use reverser routing.
+ * - `Router::url(['controller' => 'Posts', 'action' => 'edit']);` Returns a URL
+ * generated through reverse routing.
+ * - `Router::url(['_name' => 'custom-name', ...]);` Returns a URL generated
+ * through reverse routing. This form allows you to leverage named routes.
+ *
+ * There are a few 'special' parameters that can change the final URL string that is generated
+ *
+ * - `_base` - Set to false to remove the base path from the generated URL. If your application
+ * is not in the root directory, this can be used to generate URLs that are 'cake relative'.
+ * cake relative URLs are required when using requestAction.
+ * - `_scheme` - Set to create links on different schemes like `webcal` or `ftp`. Defaults
+ * to the current scheme.
+ * - `_host` - Set the host to use for the link. Defaults to the current host.
+ * - `_port` - Set the port if you need to create links on non-standard ports.
+ * - `_full` - If true output of `Router::fullBaseUrl()` will be prepended to generated URLs.
+ * - `#` - Allows you to set URL hash fragments.
+ * - `_ssl` - Set to true to convert the generated URL to https, or false to force http.
+ * - `_name` - Name of route. If you have setup named routes you can use this key
+ * to specify it.
+ *
+ * @param string|array|\Psr\Http\Message\UriInterface|null $url An array specifying any of the following:
+ * 'controller', 'action', 'plugin' additionally, you can provide routed
+ * elements or query string parameters. If string it can be name any valid url
+ * string or it can be an UriInterface instance.
+ * @param bool $full If true, the full base URL will be prepended to the result.
+ * Default is false.
+ * @return string Full translated URL with base path.
+ * @throws \Cake\Core\Exception\CakeException When the route name is not found
+ */
+ public static function url($url = null, bool $full = false): string
+ {
+ $context = static::$_requestContext;
+ $request = static::getRequest();
+
+ if (!isset($context['_base'])) {
+ $context['_base'] = Configure::read('App.base') ?: '';
+ }
+
+ if (empty($url)) {
+ $here = $request ? $request->getRequestTarget() : '/';
+ $output = $context['_base'] . $here;
+ if ($full) {
+ $output = static::fullBaseUrl() . $output;
+ }
+
+ return $output;
+ }
+
+ $params = [
+ 'plugin' => null,
+ 'controller' => null,
+ 'action' => 'index',
+ '_ext' => null,
+ ];
+ if (!empty($context['params'])) {
+ $params = $context['params'];
+ }
+
+ $frag = '';
+
+ if (is_array($url)) {
+ if (isset($url['_path'])) {
+ $url = self::unwrapShortString($url);
+ }
+
+ if (isset($url['_ssl'])) {
+ $url['_scheme'] = $url['_ssl'] === true ? 'https' : 'http';
+ }
+
+ if (isset($url['_full']) && $url['_full'] === true) {
+ $full = true;
+ }
+ if (isset($url['#'])) {
+ $frag = '#' . $url['#'];
+ }
+ unset($url['_ssl'], $url['_full'], $url['#']);
+
+ $url = static::_applyUrlFilters($url);
+
+ if (!isset($url['_name'])) {
+ // Copy the current action if the controller is the current one.
+ if (
+ empty($url['action']) &&
+ (
+ empty($url['controller']) ||
+ $params['controller'] === $url['controller']
+ )
+ ) {
+ $url['action'] = $params['action'];
+ }
+
+ // Keep the current prefix around if none set.
+ if (isset($params['prefix']) && !isset($url['prefix'])) {
+ $url['prefix'] = $params['prefix'];
+ }
+
+ $url += [
+ 'plugin' => $params['plugin'],
+ 'controller' => $params['controller'],
+ 'action' => 'index',
+ '_ext' => null,
+ ];
+ }
+
+ // If a full URL is requested with a scheme the host should default
+ // to App.fullBaseUrl to avoid corrupt URLs
+ if ($full && isset($url['_scheme']) && !isset($url['_host'])) {
+ $url['_host'] = $context['_host'];
+ }
+ $context['params'] = $params;
+
+ $output = static::$_collection->match($url, $context);
+ } else {
+ $url = (string)$url;
+
+ $plainString = (
+ strpos($url, 'javascript:') === 0 ||
+ strpos($url, 'mailto:') === 0 ||
+ strpos($url, 'tel:') === 0 ||
+ strpos($url, 'sms:') === 0 ||
+ strpos($url, '#') === 0 ||
+ strpos($url, '?') === 0 ||
+ strpos($url, '//') === 0 ||
+ strpos($url, '://') !== false
+ );
+
+ if ($plainString) {
+ return $url;
+ }
+ $output = $context['_base'] . $url;
+ }
+
+ $protocol = preg_match('#^[a-z][a-z0-9+\-.]*\://#i', $output);
+ if ($protocol === 0) {
+ $output = str_replace('//', '/', '/' . $output);
+ if ($full) {
+ $output = static::fullBaseUrl() . $output;
+ }
+ }
+
+ return $output . $frag;
+ }
+
+ /**
+ * Generate URL for route path.
+ *
+ * Route path examples:
+ * - Bookmarks::view
+ * - Admin/Bookmarks::view
+ * - Cms.Articles::edit
+ * - Vendor/Cms.Management/Admin/Articles::view
+ *
+ * @param string $path Route path specifying controller and action, optionally with plugin and prefix.
+ * @param array $params An array specifying any additional parameters.
+ * Can be also any special parameters supported by `Router::url()`.
+ * @param bool $full If true, the full base URL will be prepended to the result.
+ * Default is false.
+ * @return string Full translated URL with base path.
+ */
+ public static function pathUrl(string $path, array $params = [], bool $full = false): string
+ {
+ return static::url(['_path' => $path] + $params, $full);
+ }
+
+ /**
+ * Finds URL for specified action.
+ *
+ * Returns a bool if the url exists
+ *
+ * ### Usage
+ *
+ * @see Router::url()
+ * @param string|array|null $url An array specifying any of the following:
+ * 'controller', 'action', 'plugin' additionally, you can provide routed
+ * elements or query string parameters. If string it can be name any valid url
+ * string.
+ * @param bool $full If true, the full base URL will be prepended to the result.
+ * Default is false.
+ * @return bool
+ */
+ public static function routeExists($url = null, bool $full = false): bool
+ {
+ try {
+ static::url($url, $full);
+
+ return true;
+ } catch (MissingRouteException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets the full base URL that will be used as a prefix for generating
+ * fully qualified URLs for this application. If no parameters are passed,
+ * the currently configured value is returned.
+ *
+ * ### Note:
+ *
+ * If you change the configuration value `App.fullBaseUrl` during runtime
+ * and expect the router to produce links using the new setting, you are
+ * required to call this method passing such value again.
+ *
+ * @param string|null $base the prefix for URLs generated containing the domain.
+ * For example: `http://example.com`
+ * @return string
+ */
+ public static function fullBaseUrl(?string $base = null): string
+ {
+ if ($base === null && static::$_fullBaseUrl !== null) {
+ return static::$_fullBaseUrl;
+ }
+
+ if ($base !== null) {
+ static::$_fullBaseUrl = $base;
+ Configure::write('App.fullBaseUrl', $base);
+ } else {
+ $base = (string)Configure::read('App.fullBaseUrl');
+
+ // If App.fullBaseUrl is empty but context is set from request through setRequest()
+ if (!$base && !empty(static::$_requestContext['_host'])) {
+ $base = sprintf(
+ '%s://%s',
+ static::$_requestContext['_scheme'],
+ static::$_requestContext['_host']
+ );
+ if (!empty(static::$_requestContext['_port'])) {
+ $base .= ':' . static::$_requestContext['_port'];
+ }
+
+ Configure::write('App.fullBaseUrl', $base);
+
+ return static::$_fullBaseUrl = $base;
+ }
+
+ static::$_fullBaseUrl = $base;
+ }
+
+ $parts = parse_url(static::$_fullBaseUrl);
+ static::$_requestContext = [
+ '_scheme' => $parts['scheme'] ?? null,
+ '_host' => $parts['host'] ?? null,
+ '_port' => $parts['port'] ?? null,
+ ] + static::$_requestContext;
+
+ return static::$_fullBaseUrl;
+ }
+
+ /**
+ * Reverses a parsed parameter array into an array.
+ *
+ * Works similarly to Router::url(), but since parsed URL's contain additional
+ * keys like 'pass', '_matchedRoute' etc. those keys need to be specially
+ * handled in order to reverse a params array into a string URL.
+ *
+ * @param \Cake\Http\ServerRequest|array $params The params array or
+ * Cake\Http\ServerRequest object that needs to be reversed.
+ * @return array The URL array ready to be used for redirect or HTML link.
+ */
+ public static function reverseToArray($params): array
+ {
+ if ($params instanceof ServerRequest) {
+ $queryString = $params->getQueryParams();
+ $params = $params->getAttribute('params');
+ $params['?'] = $queryString;
+ }
+ $pass = $params['pass'] ?? [];
+
+ unset(
+ $params['pass'],
+ $params['_matchedRoute'],
+ $params['_name']
+ );
+ $params = array_merge($params, $pass);
+
+ return $params;
+ }
+
+ /**
+ * Reverses a parsed parameter array into a string.
+ *
+ * Works similarly to Router::url(), but since parsed URL's contain additional
+ * keys like 'pass', '_matchedRoute' etc. those keys need to be specially
+ * handled in order to reverse a params array into a string URL.
+ *
+ * @param \Cake\Http\ServerRequest|array $params The params array or
+ * Cake\Http\ServerRequest object that needs to be reversed.
+ * @param bool $full Set to true to include the full URL including the
+ * protocol when reversing the URL.
+ * @return string The string that is the reversed result of the array
+ */
+ public static function reverse($params, $full = false): string
+ {
+ $params = static::reverseToArray($params);
+
+ return static::url($params, $full);
+ }
+
+ /**
+ * Normalizes a URL for purposes of comparison.
+ *
+ * Will strip the base path off and replace any double /'s.
+ * It will not unify the casing and underscoring of the input value.
+ *
+ * @param array|string $url URL to normalize Either an array or a string URL.
+ * @return string Normalized URL
+ */
+ public static function normalize($url = '/'): string
+ {
+ if (is_array($url)) {
+ $url = static::url($url);
+ }
+ if (preg_match('/^[a-z\-]+:\/\//', $url)) {
+ return $url;
+ }
+ $request = static::getRequest();
+
+ if ($request) {
+ $base = $request->getAttribute('base');
+ if (strlen($base) && stristr($url, $base)) {
+ $url = preg_replace('/^' . preg_quote($base, '/') . '/', '', $url, 1);
+ }
+ }
+ $url = '/' . $url;
+
+ while (strpos($url, '//') !== false) {
+ $url = str_replace('//', '/', $url);
+ }
+ $url = preg_replace('/(?:(\/$))/', '', $url);
+
+ if (empty($url)) {
+ return '/';
+ }
+
+ return $url;
+ }
+
+ /**
+ * Get or set valid extensions for all routes connected later.
+ *
+ * Instructs the router to parse out file extensions
+ * from the URL. For example, http://example.com/posts.rss would yield a file
+ * extension of "rss". The file extension itself is made available in the
+ * controller as `$this->request->getParam('_ext')`, and is used by the RequestHandler
+ * component to automatically switch to alternate layouts and templates, and
+ * load helpers corresponding to the given content, i.e. RssHelper. Switching
+ * layouts and helpers requires that the chosen extension has a defined mime type
+ * in `Cake\Http\Response`.
+ *
+ * A string or an array of valid extensions can be passed to this method.
+ * If called without any parameters it will return current list of set extensions.
+ *
+ * @param string[]|string|null $extensions List of extensions to be added.
+ * @param bool $merge Whether to merge with or override existing extensions.
+ * Defaults to `true`.
+ * @return string[] Array of extensions Router is configured to parse.
+ */
+ public static function extensions($extensions = null, $merge = true): array
+ {
+ $collection = static::$_collection;
+ if ($extensions === null) {
+ return array_unique(array_merge(static::$_defaultExtensions, $collection->getExtensions()));
+ }
+ $extensions = (array)$extensions;
+ if ($merge) {
+ $extensions = array_unique(array_merge(static::$_defaultExtensions, $extensions));
+ }
+
+ return static::$_defaultExtensions = $extensions;
+ }
+
+ /**
+ * Create a RouteBuilder for the provided path.
+ *
+ * @param string $path The path to set the builder to.
+ * @param array $options The options for the builder
+ * @return \Cake\Routing\RouteBuilder
+ */
+ public static function createRouteBuilder(string $path, array $options = []): RouteBuilder
+ {
+ $defaults = [
+ 'routeClass' => static::defaultRouteClass(),
+ 'extensions' => static::$_defaultExtensions,
+ ];
+ $options += $defaults;
+
+ return new RouteBuilder(static::$_collection, $path, [], [
+ 'routeClass' => $options['routeClass'],
+ 'extensions' => $options['extensions'],
+ ]);
+ }
+
+ /**
+ * Create a routing scope.
+ *
+ * Routing scopes allow you to keep your routes DRY and avoid repeating
+ * common path prefixes, and or parameter sets.
+ *
+ * Scoped collections will be indexed by path for faster route parsing. If you
+ * re-open or re-use a scope the connected routes will be merged with the
+ * existing ones.
+ *
+ * ### Options
+ *
+ * The `$params` array allows you to define options for the routing scope.
+ * The options listed below *are not* available to be used as routing defaults
+ *
+ * - `routeClass` The route class to use in this scope. Defaults to
+ * `Router::defaultRouteClass()`
+ * - `extensions` The extensions to enable in this scope. Defaults to the globally
+ * enabled extensions set with `Router::extensions()`
+ *
+ * ### Example
+ *
+ * ```
+ * Router::scope('/blog', ['plugin' => 'Blog'], function ($routes) {
+ * $routes->connect('/', ['controller' => 'Articles']);
+ * });
+ * ```
+ *
+ * The above would result in a `/blog/` route being created, with both the
+ * plugin & controller default parameters set.
+ *
+ * You can use `Router::plugin()` and `Router::prefix()` as shortcuts to creating
+ * specific kinds of scopes.
+ *
+ * @param string $path The path prefix for the scope. This path will be prepended
+ * to all routes connected in the scoped collection.
+ * @param array|callable $params An array of routing defaults to add to each connected route.
+ * If you have no parameters, this argument can be a callable.
+ * @param callable|null $callback The callback to invoke with the scoped collection.
+ * @throws \InvalidArgumentException When an invalid callable is provided.
+ * @return void
+ */
+ public static function scope(string $path, $params = [], $callback = null): void
+ {
+ $options = [];
+ if (is_array($params)) {
+ $options = $params;
+ unset($params['routeClass'], $params['extensions']);
+ }
+ $builder = static::createRouteBuilder('/', $options);
+ $builder->scope($path, $params, $callback);
+ }
+
+ /**
+ * Create prefixed routes.
+ *
+ * This method creates a scoped route collection that includes
+ * relevant prefix information.
+ *
+ * The path parameter is used to generate the routing parameter name.
+ * For example a path of `admin` would result in `'prefix' => 'Admin'` being
+ * applied to all connected routes.
+ *
+ * The prefix name will be inflected to the dasherized version to create
+ * the routing path. If you want a custom path name, use the `path` option.
+ *
+ * You can re-open a prefix as many times as necessary, as well as nest prefixes.
+ * Nested prefixes will result in prefix values like `Admin/Api` which translates
+ * to the `Controller\Admin\Api\` namespace.
+ *
+ * @param string $name The prefix name to use.
+ * @param array|callable $params An array of routing defaults to add to each connected route.
+ * If you have no parameters, this argument can be a callable.
+ * @param callable|null $callback The callback to invoke that builds the prefixed routes.
+ * @return void
+ */
+ public static function prefix(string $name, $params = [], $callback = null): void
+ {
+ if (!is_array($params)) {
+ $callback = $params;
+ $params = [];
+ }
+
+ $path = $params['path'] ?? '/' . Inflector::dasherize($name);
+ unset($params['path']);
+
+ $params = array_merge($params, ['prefix' => Inflector::camelize($name)]);
+ static::scope($path, $params, $callback);
+ }
+
+ /**
+ * Add plugin routes.
+ *
+ * This method creates a scoped route collection that includes
+ * relevant plugin information.
+ *
+ * The plugin name will be inflected to the dasherized version to create
+ * the routing path. If you want a custom path name, use the `path` option.
+ *
+ * Routes connected in the scoped collection will have the correct path segment
+ * prepended, and have a matching plugin routing key set.
+ *
+ * @param string $name The plugin name to build routes for
+ * @param array|callable $options Either the options to use, or a callback
+ * @param callable|null $callback The callback to invoke that builds the plugin routes.
+ * Only required when $options is defined
+ * @return void
+ */
+ public static function plugin(string $name, $options = [], $callback = null): void
+ {
+ if (!is_array($options)) {
+ $callback = $options;
+ $options = [];
+ }
+ $params = ['plugin' => $name];
+ $path = $options['path'] ?? '/' . Inflector::dasherize($name);
+ if (isset($options['_namePrefix'])) {
+ $params['_namePrefix'] = $options['_namePrefix'];
+ }
+ static::scope($path, $params, $callback);
+ }
+
+ /**
+ * Get the route scopes and their connected routes.
+ *
+ * @return \Cake\Routing\Route\Route[]
+ */
+ public static function routes(): array
+ {
+ return static::$_collection->routes();
+ }
+
+ /**
+ * Get the RouteCollection inside the Router
+ *
+ * @return \Cake\Routing\RouteCollection
+ */
+ public static function getRouteCollection(): RouteCollection
+ {
+ return static::$_collection;
+ }
+
+ /**
+ * Set the RouteCollection inside the Router
+ *
+ * @param \Cake\Routing\RouteCollection $routeCollection route collection
+ * @return void
+ */
+ public static function setRouteCollection(RouteCollection $routeCollection): void
+ {
+ static::$_collection = $routeCollection;
+ }
+
+ /**
+ * Inject route defaults from `_path` key
+ *
+ * @param array $url Route array with `_path` key
+ * @return array
+ */
+ protected static function unwrapShortString(array $url)
+ {
+ foreach (['plugin', 'prefix', 'controller', 'action'] as $key) {
+ if (array_key_exists($key, $url)) {
+ throw new InvalidArgumentException(
+ "`$key` cannot be used when defining route targets with a string route path."
+ );
+ }
+ }
+ $url += static::parseRoutePath($url['_path']);
+ $url += [
+ 'plugin' => false,
+ 'prefix' => false,
+ ];
+ unset($url['_path']);
+
+ return $url;
+ }
+
+ /**
+ * Parse a string route path
+ *
+ * String examples:
+ * - Bookmarks::view
+ * - Admin/Bookmarks::view
+ * - Cms.Articles::edit
+ * - Vendor/Cms.Management/Admin/Articles::view
+ *
+ * @param string $url Route path in [Plugin.][Prefix/]Controller::action format
+ * @return string[]
+ */
+ public static function parseRoutePath(string $url): array
+ {
+ if (isset(static::$_routePaths[$url])) {
+ return static::$_routePaths[$url];
+ }
+
+ $regex = '#^
+ (?:(?[a-z0-9]+(?:/[a-z0-9]+)*)\.)?
+ (?:(?[a-z0-9]+(?:/[a-z0-9]+)*)/)?
+ (?[a-z0-9]+)
+ ::
+ (?[a-z0-9_]+)
+ $#ix';
+
+ if (!preg_match($regex, $url, $matches)) {
+ throw new InvalidArgumentException("Could not parse a string route path `{$url}`.");
+ }
+
+ $defaults = [];
+
+ if ($matches['plugin'] !== '') {
+ $defaults['plugin'] = $matches['plugin'];
+ }
+ if ($matches['prefix'] !== '') {
+ $defaults['prefix'] = $matches['prefix'];
+ }
+ $defaults['controller'] = $matches['controller'];
+ $defaults['action'] = $matches['action'];
+
+ static::$_routePaths[$url] = $defaults;
+
+ return $defaults;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Routing/RoutingApplicationInterface.php b/app/vendor/cakephp/cakephp/src/Routing/RoutingApplicationInterface.php
new file mode 100644
index 000000000..d2b98cc9f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Routing/RoutingApplicationInterface.php
@@ -0,0 +1,33 @@
+ false,
+ 'prefix' => false,
+ ];
+
+ return $url + $params;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Shell/Helper/ProgressHelper.php b/app/vendor/cakephp/cakephp/src/Shell/Helper/ProgressHelper.php
new file mode 100644
index 000000000..9b49d98dd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Shell/Helper/ProgressHelper.php
@@ -0,0 +1,152 @@
+helper('Progress')->output(['callback' => function ($progress) {
+ * // Do work
+ * $progress->increment();
+ * });
+ * ```
+ */
+class ProgressHelper extends Helper
+{
+ /**
+ * The current progress.
+ *
+ * @var int|float
+ */
+ protected $_progress = 0;
+
+ /**
+ * The total number of 'items' to progress through.
+ *
+ * @var int
+ */
+ protected $_total = 0;
+
+ /**
+ * The width of the bar.
+ *
+ * @var int
+ */
+ protected $_width = 0;
+
+ /**
+ * Output a progress bar.
+ *
+ * Takes a number of options to customize the behavior:
+ *
+ * - `total` The total number of items in the progress bar. Defaults
+ * to 100.
+ * - `width` The width of the progress bar. Defaults to 80.
+ * - `callback` The callback that will be called in a loop to advance the progress bar.
+ *
+ * @param array $args The arguments/options to use when outputing the progress bar.
+ * @return void
+ */
+ public function output(array $args): void
+ {
+ $args += ['callback' => null];
+ if (isset($args[0])) {
+ $args['callback'] = $args[0];
+ }
+ if (!$args['callback'] || !is_callable($args['callback'])) {
+ throw new RuntimeException('Callback option must be a callable.');
+ }
+ $this->init($args);
+
+ $callback = $args['callback'];
+
+ $this->_io->out('', 0);
+ while ($this->_progress < $this->_total) {
+ $callback($this);
+ $this->draw();
+ }
+ $this->_io->out('');
+ }
+
+ /**
+ * Initialize the progress bar for use.
+ *
+ * - `total` The total number of items in the progress bar. Defaults
+ * to 100.
+ * - `width` The width of the progress bar. Defaults to 80.
+ *
+ * @param array $args The initialization data.
+ * @return $this
+ */
+ public function init(array $args = [])
+ {
+ $args += ['total' => 100, 'width' => 80];
+ $this->_progress = 0;
+ $this->_width = $args['width'];
+ $this->_total = $args['total'];
+
+ return $this;
+ }
+
+ /**
+ * Increment the progress bar.
+ *
+ * @param int|float $num The amount of progress to advance by.
+ * @return $this
+ */
+ public function increment($num = 1)
+ {
+ $this->_progress = min(max(0, $this->_progress + $num), $this->_total);
+
+ return $this;
+ }
+
+ /**
+ * Render the progress bar based on the current state.
+ *
+ * @return $this
+ */
+ public function draw()
+ {
+ $numberLen = strlen(' 100%');
+ $complete = round($this->_progress / $this->_total, 2);
+ $barLen = ($this->_width - $numberLen) * $this->_progress / $this->_total;
+ $bar = '';
+ if ($barLen > 1) {
+ $bar = str_repeat('=', (int)$barLen - 1) . '>';
+ }
+
+ $pad = ceil($this->_width - $numberLen - $barLen);
+ if ($pad > 0) {
+ $bar .= str_repeat(' ', (int)$pad);
+ }
+ $percent = ($complete * 100) . '%';
+ $bar .= str_pad($percent, $numberLen, ' ', STR_PAD_LEFT);
+
+ $this->_io->overwrite($bar, 0);
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Shell/Helper/TableHelper.php b/app/vendor/cakephp/cakephp/src/Shell/Helper/TableHelper.php
new file mode 100644
index 000000000..b384e9fbc
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Shell/Helper/TableHelper.php
@@ -0,0 +1,185 @@
+ true,
+ 'rowSeparator' => false,
+ 'headerStyle' => 'info',
+ ];
+
+ /**
+ * Calculate the column widths
+ *
+ * @param array $rows The rows on which the columns width will be calculated on.
+ * @return int[]
+ */
+ protected function _calculateWidths(array $rows): array
+ {
+ $widths = [];
+ foreach ($rows as $line) {
+ foreach (array_values($line) as $k => $v) {
+ $columnLength = $this->_cellWidth((string)$v);
+ if ($columnLength >= ($widths[$k] ?? 0)) {
+ $widths[$k] = $columnLength;
+ }
+ }
+ }
+
+ return $widths;
+ }
+
+ /**
+ * Get the width of a cell exclusive of style tags.
+ *
+ * @param string $text The text to calculate a width for.
+ * @return int The width of the textual content in visible characters.
+ */
+ protected function _cellWidth(string $text): int
+ {
+ if (strlen($text) === 0) {
+ return 0;
+ }
+
+ if (strpos($text, '<') === false && strpos($text, '>') === false) {
+ return mb_strwidth($text);
+ }
+
+ $styles = $this->_io->styles();
+ $tags = implode('|', array_keys($styles));
+ $text = preg_replace('#?(?:' . $tags . ')>#', '', $text);
+
+ return mb_strwidth($text);
+ }
+
+ /**
+ * Output a row separator.
+ *
+ * @param int[] $widths The widths of each column to output.
+ * @return void
+ */
+ protected function _rowSeparator(array $widths): void
+ {
+ $out = '';
+ foreach ($widths as $column) {
+ $out .= '+' . str_repeat('-', $column + 2);
+ }
+ $out .= '+';
+ $this->_io->out($out);
+ }
+
+ /**
+ * Output a row.
+ *
+ * @param array $row The row to output.
+ * @param int[] $widths The widths of each column to output.
+ * @param array $options Options to be passed.
+ * @return void
+ */
+ protected function _render(array $row, array $widths, array $options = []): void
+ {
+ if (count($row) === 0) {
+ return;
+ }
+
+ $out = '';
+ foreach (array_values($row) as $i => $column) {
+ $column = (string)$column;
+ $pad = $widths[$i] - $this->_cellWidth($column);
+ if (!empty($options['style'])) {
+ $column = $this->_addStyle($column, $options['style']);
+ }
+ if (strlen($column) > 0 && preg_match('#(.*).+ (.*)#', $column, $matches)) {
+ if ($matches[1] !== '' || $matches[2] !== '') {
+ throw new UnexpectedValueException('You cannot include text before or after the text-right tag.');
+ }
+ $column = str_replace(['', ' '], '', $column);
+ $out .= '| ' . str_repeat(' ', $pad) . $column . ' ';
+ } else {
+ $out .= '| ' . $column . str_repeat(' ', $pad) . ' ';
+ }
+ }
+ $out .= '|';
+ $this->_io->out($out);
+ }
+
+ /**
+ * Output a table.
+ *
+ * Data will be output based on the order of the values
+ * in the array. The keys will not be used to align data.
+ *
+ * @param array $args The data to render out.
+ * @return void
+ */
+ public function output(array $args): void
+ {
+ if (empty($args)) {
+ return;
+ }
+
+ $this->_io->setStyle('text-right', ['text' => null]);
+
+ $config = $this->getConfig();
+ $widths = $this->_calculateWidths($args);
+
+ $this->_rowSeparator($widths);
+ if ($config['headers'] === true) {
+ $this->_render(array_shift($args), $widths, ['style' => $config['headerStyle']]);
+ $this->_rowSeparator($widths);
+ }
+
+ if (empty($args)) {
+ return;
+ }
+
+ foreach ($args as $line) {
+ $this->_render($line, $widths);
+ if ($config['rowSeparator'] === true) {
+ $this->_rowSeparator($widths);
+ }
+ }
+ if ($config['rowSeparator'] !== true) {
+ $this->_rowSeparator($widths);
+ }
+ }
+
+ /**
+ * Add style tags
+ *
+ * @param string $text The text to be surrounded
+ * @param string $style The style to be applied
+ * @return string
+ */
+ protected function _addStyle(string $text, string $style): string
+ {
+ return '<' . $style . '>' . $text . '' . $style . '>';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Shell/Task/CommandTask.php b/app/vendor/cakephp/cakephp/src/Shell/Task/CommandTask.php
new file mode 100644
index 000000000..e41434df4
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Shell/Task/CommandTask.php
@@ -0,0 +1,132 @@
+ null, 'app' => null];
+
+ $appPath = App::classPath('Shell');
+ $shellList = $this->_findShells($shellList, $appPath[0], 'app', $skipFiles);
+
+ $appPath = App::classPath('Command');
+ $shellList = $this->_findShells($shellList, $appPath[0], 'app', $skipFiles);
+
+ $skipCore = array_merge($skipFiles, $hiddenCommands, $shellList['app']);
+ $corePath = dirname(__DIR__);
+ $shellList = $this->_findShells($shellList, $corePath, 'CORE', $skipCore);
+
+ $corePath = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'Command';
+ $shellList = $this->_findShells($shellList, $corePath, 'CORE', $skipCore);
+
+ foreach ($plugins as $plugin) {
+ $pluginPath = Plugin::classPath($plugin) . 'Shell';
+ $shellList = $this->_findShells($shellList, $pluginPath, $plugin, []);
+ }
+
+ return array_filter($shellList);
+ }
+
+ /**
+ * Find shells in $path and add them to $shellList
+ *
+ * @param array $shellList The shell listing array.
+ * @param string $path The path to look in.
+ * @param string $key The key to add shells to
+ * @param string[] $skip A list of commands to exclude.
+ * @return array The updated list of shells.
+ */
+ protected function _findShells(array $shellList, string $path, string $key, array $skip): array
+ {
+ $shells = $this->_scanDir($path);
+
+ return $this->_appendShells($key, $shells, $shellList, $skip);
+ }
+
+ /**
+ * Scan the provided paths for shells, and append them into $shellList
+ *
+ * @param string $type The type of object.
+ * @param string[] $shells The shell names.
+ * @param array $shellList List of shells.
+ * @param string[] $skip List of command names to skip.
+ * @return array The updated $shellList
+ */
+ protected function _appendShells(string $type, array $shells, array $shellList, array $skip): array
+ {
+ if (!isset($shellList[$type])) {
+ $shellList[$type] = [];
+ }
+
+ foreach ($shells as $shell) {
+ $name = Inflector::underscore(preg_replace('/(Shell|Command)$/', '', $shell));
+ if (!in_array($name, $skip, true)) {
+ $shellList[$type][] = $name;
+ }
+ }
+ sort($shellList[$type]);
+
+ return $shellList;
+ }
+
+ /**
+ * Scan a directory for .php files and return the class names that
+ * should be within them.
+ *
+ * @param string $dir The directory to read.
+ * @return array The list of shell classnames based on conventions.
+ */
+ protected function _scanDir(string $dir): array
+ {
+ if (!is_dir($dir)) {
+ return [];
+ }
+
+ $fs = new Filesystem();
+ $files = $fs->find($dir, '/\.php$/');
+
+ $shells = [];
+ foreach ($files as $file) {
+ $shells[] = $file->getBasename('.php');
+ }
+
+ sort($shells);
+
+ return $shells;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/ConsoleIntegrationTestCase.php b/app/vendor/cakephp/cakephp/src/TestSuite/ConsoleIntegrationTestCase.php
new file mode 100644
index 000000000..5e42cdeae
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/ConsoleIntegrationTestCase.php
@@ -0,0 +1,27 @@
+makeRunner();
+
+ if ($this->_out === null) {
+ $this->_out = new ConsoleOutput();
+ }
+ if ($this->_err === null) {
+ $this->_err = new ConsoleOutput();
+ }
+ if ($this->_in === null) {
+ $this->_in = new ConsoleInput($input);
+ } elseif ($input) {
+ throw new RuntimeException('You can use `$input` only if `$_in` property is null and will be reset.');
+ }
+
+ $args = $this->commandStringToArgs("cake $command");
+ $io = new ConsoleIo($this->_out, $this->_err, $this->_in);
+
+ try {
+ $this->_exitCode = $runner->run($args, $io);
+ } catch (MissingConsoleInputException $e) {
+ $messages = $this->_out->messages();
+ if (count($messages)) {
+ $e->setQuestion($messages[count($messages) - 1]);
+ }
+ throw $e;
+ } catch (StopException $exception) {
+ $this->_exitCode = $exception->getCode();
+ }
+ }
+
+ /**
+ * Cleans state to get ready for the next test
+ *
+ * @after
+ * @return void
+ * @psalm-suppress PossiblyNullPropertyAssignmentValue
+ */
+ public function cleanupConsoleTrait(): void
+ {
+ $this->_exitCode = null;
+ $this->_out = null;
+ $this->_err = null;
+ $this->_in = null;
+ $this->_useCommandRunner = false;
+ }
+
+ /**
+ * Set this test case to use the CommandRunner rather than the legacy
+ * ShellDispatcher
+ *
+ * @return void
+ */
+ public function useCommandRunner(): void
+ {
+ $this->_useCommandRunner = true;
+ }
+
+ /**
+ * Asserts shell exited with the expected code
+ *
+ * @param int $expected Expected exit code
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertExitCode(int $expected, string $message = ''): void
+ {
+ $this->assertThat($expected, new ExitCode($this->_exitCode), $message);
+ }
+
+ /**
+ * Asserts shell exited with the Command::CODE_SUCCESS
+ *
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertExitSuccess($message = '')
+ {
+ $this->assertThat(Command::CODE_SUCCESS, new ExitCode($this->_exitCode), $message);
+ }
+
+ /**
+ * Asserts shell exited with Command::CODE_ERROR
+ *
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertExitError($message = '')
+ {
+ $this->assertThat(Command::CODE_ERROR, new ExitCode($this->_exitCode), $message);
+ }
+
+ /**
+ * Asserts that `stdout` is empty
+ *
+ * @param string $message The message to output when the assertion fails.
+ * @return void
+ */
+ public function assertOutputEmpty(string $message = ''): void
+ {
+ $this->assertThat(null, new ContentsEmpty($this->_out->messages(), 'output'), $message);
+ }
+
+ /**
+ * Asserts `stdout` contains expected output
+ *
+ * @param string $expected Expected output
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertOutputContains(string $expected, string $message = ''): void
+ {
+ $this->assertThat($expected, new ContentsContain($this->_out->messages(), 'output'), $message);
+ }
+
+ /**
+ * Asserts `stdout` does not contain expected output
+ *
+ * @param string $expected Expected output
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertOutputNotContains(string $expected, string $message = ''): void
+ {
+ $this->assertThat($expected, new ContentsNotContain($this->_out->messages(), 'output'), $message);
+ }
+
+ /**
+ * Asserts `stdout` contains expected regexp
+ *
+ * @param string $pattern Expected pattern
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertOutputRegExp(string $pattern, string $message = ''): void
+ {
+ $this->assertThat($pattern, new ContentsRegExp($this->_out->messages(), 'output'), $message);
+ }
+
+ /**
+ * Check that a row of cells exists in the output.
+ *
+ * @param array $row Row of cells to ensure exist in the output.
+ * @param string $message Failure message.
+ * @return void
+ */
+ protected function assertOutputContainsRow(array $row, string $message = ''): void
+ {
+ $this->assertThat($row, new ContentsContainRow($this->_out->messages(), 'output'), $message);
+ }
+
+ /**
+ * Asserts `stderr` contains expected output
+ *
+ * @param string $expected Expected output
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertErrorContains(string $expected, string $message = ''): void
+ {
+ $this->assertThat($expected, new ContentsContain($this->_err->messages(), 'error output'), $message);
+ }
+
+ /**
+ * Asserts `stderr` contains expected regexp
+ *
+ * @param string $pattern Expected pattern
+ * @param string $message Failure message
+ * @return void
+ */
+ public function assertErrorRegExp(string $pattern, string $message = ''): void
+ {
+ $this->assertThat($pattern, new ContentsRegExp($this->_err->messages(), 'error output'), $message);
+ }
+
+ /**
+ * Asserts that `stderr` is empty
+ *
+ * @param string $message The message to output when the assertion fails.
+ * @return void
+ */
+ public function assertErrorEmpty(string $message = ''): void
+ {
+ $this->assertThat(null, new ContentsEmpty($this->_err->messages(), 'error output'), $message);
+ }
+
+ /**
+ * Builds the appropriate command dispatcher
+ *
+ * @return \Cake\Console\CommandRunner|\Cake\TestSuite\LegacyCommandRunner
+ */
+ protected function makeRunner()
+ {
+ if ($this->_useCommandRunner) {
+ /** @var \Cake\Core\ConsoleApplicationInterface $app */
+ $app = $this->createApp();
+
+ return new CommandRunner($app);
+ }
+
+ return new LegacyCommandRunner();
+ }
+
+ /**
+ * Creates an $argv array from a command string
+ *
+ * @param string $command Command string
+ * @return string[]
+ */
+ protected function commandStringToArgs(string $command): array
+ {
+ $charCount = strlen($command);
+ $argv = [];
+ $arg = '';
+ $inDQuote = false;
+ $inSQuote = false;
+ for ($i = 0; $i < $charCount; $i++) {
+ $char = substr($command, $i, 1);
+
+ // end of argument
+ if ($char === ' ' && !$inDQuote && !$inSQuote) {
+ if (strlen($arg)) {
+ $argv[] = $arg;
+ }
+ $arg = '';
+ continue;
+ }
+
+ // exiting single quote
+ if ($inSQuote && $char === "'") {
+ $inSQuote = false;
+ continue;
+ }
+
+ // exiting double quote
+ if ($inDQuote && $char === '"') {
+ $inDQuote = false;
+ continue;
+ }
+
+ // entering double quote
+ if ($char === '"' && !$inSQuote) {
+ $inDQuote = true;
+ continue;
+ }
+
+ // entering single quote
+ if ($char === "'" && !$inDQuote) {
+ $inSQuote = true;
+ continue;
+ }
+
+ $arg .= $char;
+ }
+ $argv[] = $arg;
+
+ return $argv;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsBase.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsBase.php
new file mode 100644
index 000000000..f58953829
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsBase.php
@@ -0,0 +1,48 @@
+contents = implode(PHP_EOL, $contents);
+ $this->output = $output;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsContain.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsContain.php
new file mode 100644
index 000000000..d700c9664
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsContain.php
@@ -0,0 +1,45 @@
+contents, $other) !== false;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('is in %s,' . PHP_EOL . 'actual result:' . PHP_EOL, $this->output) . $this->contents;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsContainRow.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsContainRow.php
new file mode 100644
index 000000000..3ee85920e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsContainRow.php
@@ -0,0 +1,61 @@
+contents) > 0;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('row was in %s', $this->output);
+ }
+
+ /**
+ * @param mixed $other Expected content
+ * @return string
+ */
+ public function failureDescription($other): string
+ {
+ return '`' . $this->exporter()->shortenedExport($other) . '` ' . $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsEmpty.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsEmpty.php
new file mode 100644
index 000000000..cbc96d2ce
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsEmpty.php
@@ -0,0 +1,56 @@
+contents === '';
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('%s is empty', $this->output);
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsNotContain.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsNotContain.php
new file mode 100644
index 000000000..9515c37d9
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsNotContain.php
@@ -0,0 +1,45 @@
+contents, $other) === false;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('is not in %s', $this->output);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsRegExp.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsRegExp.php
new file mode 100644
index 000000000..e97b08540
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ContentsRegExp.php
@@ -0,0 +1,54 @@
+contents) > 0;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('PCRE pattern found in %s', $this->output);
+ }
+
+ /**
+ * @param mixed $other Expected
+ * @return string
+ */
+ public function failureDescription($other): string
+ {
+ return '`' . $other . '` ' . $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ExitCode.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ExitCode.php
new file mode 100644
index 000000000..660b629f3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Console/ExitCode.php
@@ -0,0 +1,62 @@
+exitCode = $exitCode;
+ }
+
+ /**
+ * Checks if event is in fired array
+ *
+ * @param mixed $other Constraint check
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ return $other === $this->exitCode;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('matches exit code %s', $this->exitCode ?? 'null');
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailConstraintBase.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailConstraintBase.php
new file mode 100644
index 000000000..fcb26812d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailConstraintBase.php
@@ -0,0 +1,64 @@
+at = $at;
+ }
+
+ /**
+ * Gets the email or emails to check
+ *
+ * @return \Cake\Mailer\Message[]
+ */
+ public function getMessages()
+ {
+ $messages = TestEmailTransport::getMessages();
+
+ if ($this->at !== null) {
+ if (!isset($messages[$this->at])) {
+ return [];
+ }
+
+ return [$messages[$this->at]];
+ }
+
+ return $messages;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContains.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContains.php
new file mode 100644
index 000000000..783c31cd2
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContains.php
@@ -0,0 +1,98 @@
+getMessages();
+ foreach ($messages as $message) {
+ $method = $this->getTypeMethod();
+ $message = $message->$method();
+
+ $other = preg_quote($other, '/');
+ if (preg_match("/$other/", $message) > 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getTypeMethod(): string
+ {
+ return 'getBody' . ($this->type ? ucfirst($this->type) : 'String');
+ }
+
+ /**
+ * Returns the type-dependent strings of all messages
+ * respects $this->at
+ *
+ * @return string
+ */
+ protected function getAssertedMessages(): string
+ {
+ $messageMembers = [];
+ $messages = $this->getMessages();
+ foreach ($messages as $message) {
+ $method = $this->getTypeMethod();
+ $messageMembers[] = $message->$method();
+ }
+ if ($this->at && isset($messageMembers[$this->at - 1])) {
+ $messageMembers = [$messageMembers[$this->at - 1]];
+ }
+ $result = implode(PHP_EOL, $messageMembers);
+
+ return PHP_EOL . 'was: ' . mb_substr($result, 0, 1000);
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ if ($this->at) {
+ return sprintf('is in email #%d', $this->at) . $this->getAssertedMessages();
+ }
+
+ return 'is in an email' . $this->getAssertedMessages();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsAttachment.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsAttachment.php
new file mode 100644
index 000000000..731612482
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsAttachment.php
@@ -0,0 +1,77 @@
+getMessages();
+ foreach ($messages as $message) {
+ foreach ($message->getAttachments() as $filename => $fileInfo) {
+ if ($filename === $expectedFilename && empty($expectedFileInfo)) {
+ return true;
+ }
+ if (!empty($expectedFileInfo) && array_intersect($expectedFileInfo, $fileInfo) === $expectedFileInfo) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ if ($this->at) {
+ return sprintf('is an attachment of email #%d', $this->at);
+ }
+
+ return 'is an attachment of an email';
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ [$expectedFilename] = $other;
+
+ return '\'' . $expectedFilename . '\' ' . $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsHtml.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsHtml.php
new file mode 100644
index 000000000..70f5c869e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsHtml.php
@@ -0,0 +1,46 @@
+at) {
+ return sprintf('is in the html message of email #%d', $this->at) . $this->getAssertedMessages();
+ }
+
+ return 'is in the html message of an email' . $this->getAssertedMessages();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsText.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsText.php
new file mode 100644
index 000000000..a2c047a19
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailContainsText.php
@@ -0,0 +1,46 @@
+at) {
+ return sprintf('is in the text message of email #%d', $this->at) . $this->getAssertedMessages();
+ }
+
+ return 'is in the text message of an email' . $this->getAssertedMessages();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailCount.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailCount.php
new file mode 100644
index 000000000..d1242de59
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailCount.php
@@ -0,0 +1,46 @@
+getMessages()) === $other;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'emails were sent';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentFrom.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentFrom.php
new file mode 100644
index 000000000..6dea5243a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentFrom.php
@@ -0,0 +1,44 @@
+at) {
+ return sprintf('sent email #%d', $this->at);
+ }
+
+ return 'sent an email';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentTo.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentTo.php
new file mode 100644
index 000000000..07126e3dc
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentTo.php
@@ -0,0 +1,44 @@
+at) {
+ return sprintf('was sent email #%d', $this->at);
+ }
+
+ return 'was sent an email';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentWith.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentWith.php
new file mode 100644
index 000000000..46aa85fa2
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSentWith.php
@@ -0,0 +1,85 @@
+method = $method;
+ }
+
+ parent::__construct($at);
+ }
+
+ /**
+ * Checks constraint
+ *
+ * @param mixed $other Constraint check
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ $emails = $this->getMessages();
+ foreach ($emails as $email) {
+ $value = $email->{'get' . ucfirst($this->method)}();
+ if (
+ in_array($this->method, ['to', 'cc', 'bcc', 'from', 'replyTo', 'sender'], true)
+ && array_key_exists($other, $value)
+ ) {
+ return true;
+ }
+ if ($value === $other) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ if ($this->at) {
+ return sprintf('is in email #%d `%s`', $this->at, $this->method);
+ }
+
+ return sprintf('is in an email `%s`', $this->method);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSubjectContains.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSubjectContains.php
new file mode 100644
index 000000000..ca5d7c93f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/MailSubjectContains.php
@@ -0,0 +1,86 @@
+getMessages();
+ foreach ($messages as $message) {
+ $subject = $message->getOriginalSubject();
+ if (strpos($subject, $other) !== false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the subjects of all messages
+ * respects $this->at
+ *
+ * @return string
+ */
+ protected function getAssertedMessages(): string
+ {
+ $messageMembers = [];
+ $messages = $this->getMessages();
+ foreach ($messages as $message) {
+ $messageMembers[] = $message->getSubject();
+ }
+ if ($this->at && isset($messageMembers[$this->at - 1])) {
+ $messageMembers = [$messageMembers[$this->at - 1]];
+ }
+ $result = implode(PHP_EOL, $messageMembers);
+
+ return PHP_EOL . 'was: ' . mb_substr($result, 0, 1000);
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ if ($this->at) {
+ return sprintf('is in an email subject #%d', $this->at) . $this->getAssertedMessages();
+ }
+
+ return 'is in an email subject' . $this->getAssertedMessages();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/NoMailSent.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/NoMailSent.php
new file mode 100644
index 000000000..0d7d48a5c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Email/NoMailSent.php
@@ -0,0 +1,57 @@
+getMessages()) === 0;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'no emails were sent';
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFired.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFired.php
new file mode 100644
index 000000000..dfb29f289
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFired.php
@@ -0,0 +1,74 @@
+_eventManager = $eventManager;
+
+ if ($this->_eventManager->getEventList() === null) {
+ throw new AssertionFailedError(
+ 'The event manager you are asserting against is not configured to track events.'
+ );
+ }
+ }
+
+ /**
+ * Checks if event is in fired array
+ *
+ * @param mixed $other Constraint check
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ $list = $this->_eventManager->getEventList();
+
+ return $list === null ? false : $list->hasEvent($other);
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'was fired';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFiredWith.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFiredWith.php
new file mode 100644
index 000000000..13ad4a171
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/EventFiredWith.php
@@ -0,0 +1,116 @@
+_eventManager = $eventManager;
+ $this->_dataKey = $dataKey;
+ $this->_dataValue = $dataValue;
+
+ if ($this->_eventManager->getEventList() === null) {
+ throw new AssertionFailedError(
+ 'The event manager you are asserting against is not configured to track events.'
+ );
+ }
+ }
+
+ /**
+ * Checks if event is in fired array
+ *
+ * @param mixed $other Constraint check
+ * @return bool
+ * @throws \PHPUnit\Framework\AssertionFailedError
+ */
+ public function matches($other): bool
+ {
+ $firedEvents = [];
+ $list = $this->_eventManager->getEventList();
+ if ($list !== null) {
+ $totalEvents = count($list);
+ for ($e = 0; $e < $totalEvents; $e++) {
+ $firedEvents[] = $list[$e];
+ }
+ }
+
+ $eventGroup = collection($firedEvents)
+ ->groupBy(function (EventInterface $event): string {
+ return $event->getName();
+ })
+ ->toArray();
+
+ if (!array_key_exists($other, $eventGroup)) {
+ return false;
+ }
+
+ /** @var \Cake\Event\EventInterface[] $events */
+ $events = $eventGroup[$other];
+
+ if (count($events) > 1) {
+ throw new AssertionFailedError(sprintf(
+ 'Event "%s" was fired %d times, cannot make data assertion',
+ $other,
+ count($events)
+ ));
+ }
+
+ $event = $events[0];
+
+ if (array_key_exists($this->_dataKey, (array)$event->getData()) === false) {
+ return false;
+ }
+
+ return $event->getData($this->_dataKey) === $this->_dataValue;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'was fired with ' . $this->_dataKey . ' matching ' . (string)$this->_dataValue;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyContains.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyContains.php
new file mode 100644
index 000000000..357f6bee3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyContains.php
@@ -0,0 +1,70 @@
+ignoreCase = $ignoreCase;
+ }
+
+ /**
+ * Checks assertion
+ *
+ * @param mixed $other Expected type
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ $method = 'mb_strpos';
+ if ($this->ignoreCase) {
+ $method = 'mb_stripos';
+ }
+
+ return $method($this->_getBodyAsString(), $other) !== false;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'is in response body';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyEmpty.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyEmpty.php
new file mode 100644
index 000000000..1f74ed403
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyEmpty.php
@@ -0,0 +1,56 @@
+_getBodyAsString());
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'response body is empty';
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyEquals.php
new file mode 100644
index 000000000..5d8bfea56
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyEquals.php
@@ -0,0 +1,45 @@
+_getBodyAsString() === $other;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'matches response body';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyNotContains.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyNotContains.php
new file mode 100644
index 000000000..7914cfe3d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/BodyNotContains.php
@@ -0,0 +1,45 @@
+_getBodyAsString()) > 0;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'PCRE pattern found in response body';
+ }
+
+ /**
+ * @param mixed $other Expected
+ * @return string
+ */
+ public function failureDescription($other): string
+ {
+ return '`' . $other . '`' . ' ' . $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/ContentType.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/ContentType.php
new file mode 100644
index 000000000..93b38d912
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/ContentType.php
@@ -0,0 +1,55 @@
+response->getMimeType($other);
+ if ($alias !== false) {
+ $other = $alias;
+ }
+
+ return $other === $this->response->getType();
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'is set as the Content-Type (`' . $this->response->getType() . '`)';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieEncryptedEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieEncryptedEquals.php
new file mode 100644
index 000000000..7cc9ff58c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieEncryptedEquals.php
@@ -0,0 +1,93 @@
+key = $key;
+ $this->mode = $mode;
+ }
+
+ /**
+ * Checks assertion
+ *
+ * @param mixed $other Expected content
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ $cookie = $this->response->getCookie($this->cookieName);
+
+ return $cookie !== null && $this->_decrypt($cookie['value'], $this->mode) === $other;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('is encrypted in cookie \'%s\'', $this->cookieName);
+ }
+
+ /**
+ * Returns the encryption key
+ *
+ * @return string
+ */
+ protected function _getCookieEncryptionKey(): string
+ {
+ return $this->key;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieEquals.php
new file mode 100644
index 000000000..3b5ca1081
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieEquals.php
@@ -0,0 +1,72 @@
+cookieName = $cookieName;
+ }
+
+ /**
+ * Checks assertion
+ *
+ * @param mixed $other Expected content
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ $cookie = $this->response->getCookie($this->cookieName);
+
+ return $cookie !== null && $cookie['value'] === $other;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('is in cookie \'%s\'', $this->cookieName);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieNotSet.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieNotSet.php
new file mode 100644
index 000000000..bfd7fa7c6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/CookieNotSet.php
@@ -0,0 +1,45 @@
+response->getCookie($other);
+
+ return $cookie !== null && $cookie['value'] !== '';
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'cookie is set';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/FileSent.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/FileSent.php
new file mode 100644
index 000000000..e11e21f0c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/FileSent.php
@@ -0,0 +1,61 @@
+response->getFile() !== null;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'file was sent';
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/FileSentAs.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/FileSentAs.php
new file mode 100644
index 000000000..5c995af59
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/FileSentAs.php
@@ -0,0 +1,51 @@
+response->getFile()->getPathName() === $other;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'file was sent';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderContains.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderContains.php
new file mode 100644
index 000000000..c0e1bb181
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderContains.php
@@ -0,0 +1,49 @@
+response->getHeaderLine($this->headerName), $other) !== false;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf(
+ 'is in header \'%s\' (`%s`)',
+ $this->headerName,
+ $this->response->getHeaderLine($this->headerName)
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderEquals.php
new file mode 100644
index 000000000..35bace044
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderEquals.php
@@ -0,0 +1,67 @@
+headerName = $headerName;
+ }
+
+ /**
+ * Checks assertion
+ *
+ * @param mixed $other Expected content
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ return $this->response->getHeaderLine($this->headerName) === $other;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ $responseHeader = $this->response->getHeaderLine($this->headerName);
+
+ return sprintf('equals content in header \'%s\' (`%s`)', $this->headerName, $responseHeader);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderNotContains.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderNotContains.php
new file mode 100644
index 000000000..51f29a923
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderNotContains.php
@@ -0,0 +1,49 @@
+headerName,
+ $this->response->getHeaderLine($this->headerName)
+ );
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderNotSet.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderNotSet.php
new file mode 100644
index 000000000..671090fd8
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderNotSet.php
@@ -0,0 +1,45 @@
+headerName);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderSet.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderSet.php
new file mode 100644
index 000000000..9fc5e4843
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/HeaderSet.php
@@ -0,0 +1,76 @@
+headerName = $headerName;
+ }
+
+ /**
+ * Checks assertion
+ *
+ * @param mixed $other Expected content
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ return $this->response->hasHeader($this->headerName);
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('response has header \'%s\'', $this->headerName);
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/ResponseBase.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/ResponseBase.php
new file mode 100644
index 000000000..2ba009db5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/ResponseBase.php
@@ -0,0 +1,57 @@
+response = $response;
+ }
+
+ /**
+ * Get the response body as string
+ *
+ * @return string The response body.
+ */
+ protected function _getBodyAsString(): string
+ {
+ return (string)$this->response->getBody();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusCode.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusCode.php
new file mode 100644
index 000000000..c2387bc99
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusCode.php
@@ -0,0 +1,45 @@
+response->getStatusCode());
+ }
+
+ /**
+ * Failure description
+ *
+ * @param mixed $other Expected code
+ * @return string
+ */
+ public function failureDescription($other): string
+ {
+ return '`' . $other . '` ' . $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusCodeBase.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusCodeBase.php
new file mode 100644
index 000000000..4940081e6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusCodeBase.php
@@ -0,0 +1,75 @@
+
+ */
+abstract class StatusCodeBase extends ResponseBase
+{
+ /**
+ * @var int|array
+ * @psalm-var TCode
+ */
+ protected $code;
+
+ /**
+ * Check assertion
+ *
+ * @param int|array $other Array of min/max status codes, or a single code
+ * @return bool
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ public function matches($other): bool
+ {
+ if (!$other) {
+ $other = $this->code;
+ }
+
+ if (is_array($other)) {
+ return $this->statusCodeBetween($other[0], $other[1]);
+ }
+
+ return $this->response->getStatusCode() === $other;
+ }
+
+ /**
+ * Helper for checking status codes
+ *
+ * @param int $min Min status code (inclusive)
+ * @param int $max Max status code (inclusive)
+ * @return bool
+ */
+ protected function statusCodeBetween(int $min, int $max): bool
+ {
+ return $this->response->getStatusCode() >= $min && $this->response->getStatusCode() <= $max;
+ }
+
+ /**
+ * Overwrites the descriptions so we can remove the automatic "expected" message
+ *
+ * @param mixed $other Value
+ * @return string
+ */
+ protected function failureDescription($other): string
+ {
+ /** @psalm-suppress InternalMethod */
+ return $this->toString();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusError.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusError.php
new file mode 100644
index 000000000..189d8112c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusError.php
@@ -0,0 +1,40 @@
+>
+ */
+class StatusError extends StatusCodeBase
+{
+ /**
+ * @var array
+ */
+ protected $code = [400, 429];
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('%d is between 400 and 429', $this->response->getStatusCode());
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusFailure.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusFailure.php
new file mode 100644
index 000000000..33d4df2de
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusFailure.php
@@ -0,0 +1,40 @@
+>
+ */
+class StatusFailure extends StatusCodeBase
+{
+ /**
+ * @var array
+ */
+ protected $code = [500, 505];
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('%d is between 500 and 505', $this->response->getStatusCode());
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusOk.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusOk.php
new file mode 100644
index 000000000..8047ff918
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusOk.php
@@ -0,0 +1,40 @@
+>
+ */
+class StatusOk extends StatusCodeBase
+{
+ /**
+ * @var array
+ */
+ protected $code = [200, 204];
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('%d is between 200 and 204', $this->response->getStatusCode());
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusSuccess.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusSuccess.php
new file mode 100644
index 000000000..10b873dee
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Response/StatusSuccess.php
@@ -0,0 +1,40 @@
+>
+ */
+class StatusSuccess extends StatusCodeBase
+{
+ /**
+ * @var array
+ */
+ protected $code = [200, 308];
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('%d is between 200 and 308', $this->response->getStatusCode());
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/FlashParamEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/FlashParamEquals.php
new file mode 100644
index 000000000..065943121
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/FlashParamEquals.php
@@ -0,0 +1,114 @@
+enableRetainFlashMessages()` has been enabled for the test.';
+ throw new AssertionFailedError($message);
+ }
+
+ $this->session = $session;
+ $this->key = $key;
+ $this->param = $param;
+ $this->at = $at;
+ }
+
+ /**
+ * Compare to flash message(s)
+ *
+ * @param mixed $other Value to compare with
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ // Server::run calls Session::close at the end of the request.
+ // Which means, that we cannot use Session object here to access the session data.
+ // Call to Session::read will start new session (and will erase the data).
+
+ $messages = (array)Hash::get($_SESSION, 'Flash.' . $this->key);
+ if ($this->at) {
+ $messages = [Hash::get($_SESSION, 'Flash.' . $this->key . '.' . $this->at)];
+ }
+
+ foreach ($messages as $message) {
+ if (!isset($message[$this->param])) {
+ continue;
+ }
+ if ($message[$this->param] === $other) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Assertion message string
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ if ($this->at !== null) {
+ return sprintf('is in \'%s\' %s #%d', $this->key, $this->param, $this->at);
+ }
+
+ return sprintf('is in \'%s\' %s', $this->key, $this->param);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/SessionEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/SessionEquals.php
new file mode 100644
index 000000000..5cb470394
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/SessionEquals.php
@@ -0,0 +1,66 @@
+path = $path;
+ }
+
+ /**
+ * Compare session value
+ *
+ * @param mixed $other Value to compare with
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ // Server::run calls Session::close at the end of the request.
+ // Which means, that we cannot use Session object here to access the session data.
+ // Call to Session::read will start new session (and will erase the data).
+ return Hash::get($_SESSION, $this->path) === $other;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('is in session path \'%s\'', $this->path);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/SessionHasKey.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/SessionHasKey.php
new file mode 100644
index 000000000..2240d3e0f
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/Session/SessionHasKey.php
@@ -0,0 +1,66 @@
+path = $path;
+ }
+
+ /**
+ * Compare session value
+ *
+ * @param mixed $other Value to compare with
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ // Server::run calls Session::close at the end of the request.
+ // Which means, that we cannot use Session object here to access the session data.
+ // Call to Session::read will start new session (and will erase the data).
+ return Hash::check($_SESSION, $this->path) === true;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return 'is a path present in the session';
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/View/LayoutFileEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/View/LayoutFileEquals.php
new file mode 100644
index 000000000..650c4a968
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/View/LayoutFileEquals.php
@@ -0,0 +1,34 @@
+filename);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/View/TemplateFileEquals.php b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/View/TemplateFileEquals.php
new file mode 100644
index 000000000..d55c73f4a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Constraint/View/TemplateFileEquals.php
@@ -0,0 +1,62 @@
+filename = $filename;
+ }
+
+ /**
+ * Checks assertion
+ *
+ * @param mixed $other Expected filename
+ * @return bool
+ */
+ public function matches($other): bool
+ {
+ return strpos($this->filename, $other) !== false;
+ }
+
+ /**
+ * Assertion message
+ *
+ * @return string
+ */
+ public function toString(): string
+ {
+ return sprintf('equals template file `%s`', $this->filename);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/ContainerStubTrait.php b/app/vendor/cakephp/cakephp/src/TestSuite/ContainerStubTrait.php
new file mode 100644
index 000000000..35b68ac7a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/ContainerStubTrait.php
@@ -0,0 +1,169 @@
+|class-string<\Cake\Core\ConsoleApplicationInterface>|null
+ * @var string|null
+ */
+ protected $_appClass;
+
+ /**
+ * The customized application constructor arguments.
+ *
+ * @var array|null
+ */
+ protected $_appArgs;
+
+ /**
+ * The collection of container services.
+ *
+ * @var array
+ */
+ private $containerServices = [];
+
+ /**
+ * Configure the application class to use in integration tests.
+ *
+ * @param string $class The application class name.
+ * @param array|null $constructorArgs The constructor arguments for your application class.
+ * @return void
+ * @psalm-param class-string<\Cake\Core\HttpApplicationInterface>|class-string<\Cake\Core\ConsoleApplicationInterface> $class
+ */
+ public function configApplication(string $class, ?array $constructorArgs): void
+ {
+ $this->_appClass = $class;
+ $this->_appArgs = $constructorArgs;
+ }
+
+ /**
+ * Create an application instance.
+ *
+ * Uses the configuration set in `configApplication()`.
+ *
+ * @return \Cake\Core\HttpApplicationInterface|\Cake\Core\ConsoleApplicationInterface
+ */
+ protected function createApp()
+ {
+ if ($this->_appClass) {
+ $appClass = $this->_appClass;
+ } else {
+ /** @psalm-var class-string<\Cake\Http\BaseApplication> */
+ $appClass = Configure::read('App.namespace') . '\Application';
+ }
+ if (!class_exists($appClass)) {
+ throw new LogicException("Cannot load `{$appClass}` for use in integration testing.");
+ }
+ $appArgs = $this->_appArgs ?: [CONFIG];
+
+ $app = new $appClass(...$appArgs);
+ if (!empty($this->containerServices) && method_exists($app, 'getEventManager')) {
+ $app->getEventManager()->on('Application.buildContainer', [$this, 'modifyContainer']);
+ }
+
+ return $app;
+ }
+
+ /**
+ * Add a mocked service to the container.
+ *
+ * When the container is created the provided classname
+ * will be mapped to the factory function. The factory
+ * function will be used to create mocked services.
+ *
+ * @param string $class The class or interface you want to define.
+ * @param \Closure $factory The factory function for mocked services.
+ * @return $this
+ */
+ public function mockService(string $class, Closure $factory)
+ {
+ $this->containerServices[$class] = $factory;
+
+ return $this;
+ }
+
+ /**
+ * Remove a mocked service to the container.
+ *
+ * @param string $class The class or interface you want to remove.
+ * @return $this
+ */
+ public function removeMockService(string $class)
+ {
+ unset($this->containerServices[$class]);
+
+ return $this;
+ }
+
+ /**
+ * Wrap the application's container with one containing mocks.
+ *
+ * If any mocked services are defined, the application's container
+ * will be replaced with one containing mocks. The original
+ * container will be set as a delegate to the mock container.
+ *
+ * @param \Cake\Event\EventInterface $event The event
+ * @param \Cake\Core\ContainerInterface $container The container to wrap.
+ * @return null|\Cake\Core\ContainerInterface
+ */
+ public function modifyContainer(EventInterface $event, ContainerInterface $container): ?ContainerInterface
+ {
+ if (empty($this->containerServices)) {
+ return null;
+ }
+ foreach ($this->containerServices as $key => $factory) {
+ if ($container->has($key)) {
+ $container->extend($key)->setConcrete($factory);
+ } else {
+ $container->add($key, $factory);
+ }
+ }
+
+ return $container;
+ }
+
+ /**
+ * Clears any mocks that were defined and cleans
+ * up application class configuration.
+ *
+ * @after
+ * @return void
+ */
+ public function cleanupContainer(): void
+ {
+ $this->_appArgs = null;
+ $this->_appClass = null;
+ $this->containerServices = [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/EmailTrait.php b/app/vendor/cakephp/cakephp/src/TestSuite/EmailTrait.php
new file mode 100644
index 000000000..7d6926af5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/EmailTrait.php
@@ -0,0 +1,273 @@
+assertThat($count, new MailCount(), $message);
+ }
+
+ /**
+ * Asserts that no emails were sent
+ *
+ * @param string $message Message
+ * @return void
+ */
+ public function assertNoMailSent(string $message = ''): void
+ {
+ $this->assertThat(null, new NoMailSent(), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index was sent to an address
+ *
+ * @param int $at Email index
+ * @param string $address Email address
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSentToAt(int $at, string $address, string $message = ''): void
+ {
+ $this->assertThat($address, new MailSentTo($at), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index was sent from an address
+ *
+ * @param int $at Email index
+ * @param string $address Email address
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSentFromAt(int $at, string $address, string $message = ''): void
+ {
+ $this->assertThat($address, new MailSentFrom($at), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index contains expected contents
+ *
+ * @param int $at Email index
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailContainsAt(int $at, string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailContains($at), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index contains expected html contents
+ *
+ * @param int $at Email index
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailContainsHtmlAt(int $at, string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailContainsHtml($at), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index contains expected text contents
+ *
+ * @param int $at Email index
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailContainsTextAt(int $at, string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailContainsText($at), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index contains the expected value within an Email getter
+ *
+ * @param int $at Email index
+ * @param string $expected Contents
+ * @param string $parameter Email getter parameter (e.g. "cc", "bcc")
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSentWithAt(int $at, string $expected, string $parameter, string $message = ''): void
+ {
+ $this->assertThat($expected, new MailSentWith($at, $parameter), $message);
+ }
+
+ /**
+ * Asserts an email was sent to an address
+ *
+ * @param string $address Email address
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSentTo(string $address, string $message = ''): void
+ {
+ $this->assertThat($address, new MailSentTo(), $message);
+ }
+
+ /**
+ * Asserts an email was sent from an address
+ *
+ * @param string $address Email address
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSentFrom(string $address, string $message = ''): void
+ {
+ $this->assertThat($address, new MailSentFrom(), $message);
+ }
+
+ /**
+ * Asserts an email contains expected contents
+ *
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailContains(string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailContains(), $message);
+ }
+
+ /**
+ * Asserts an email contains expected attachment
+ *
+ * @param string $filename Filename
+ * @param array $file Additional file properties
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailContainsAttachment(string $filename, array $file = [], string $message = ''): void
+ {
+ $this->assertThat([$filename, $file], new MailContainsAttachment(), $message);
+ }
+
+ /**
+ * Asserts an email contains expected html contents
+ *
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailContainsHtml(string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailContainsHtml(), $message);
+ }
+
+ /**
+ * Asserts an email contains an expected text content
+ *
+ * @param string $expected Expected text.
+ * @param string $message Message to display if assertion fails.
+ * @return void
+ */
+ public function assertMailContainsText(string $expected, string $message = ''): void
+ {
+ $this->assertThat($expected, new MailContainsText(), $message);
+ }
+
+ /**
+ * Asserts an email contains the expected value within an Email getter
+ *
+ * @param string $expected Contents
+ * @param string $parameter Email getter parameter (e.g. "cc", "subject")
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSentWith(string $expected, string $parameter, string $message = ''): void
+ {
+ $this->assertThat($expected, new MailSentWith(null, $parameter), $message);
+ }
+
+ /**
+ * Asserts an email subject contains expected contents
+ *
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSubjectContains(string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailSubjectContains(), $message);
+ }
+
+ /**
+ * Asserts an email at a specific index contains expected html contents
+ *
+ * @param int $at Email index
+ * @param string $contents Contents
+ * @param string $message Message
+ * @return void
+ */
+ public function assertMailSubjectContainsAt(int $at, string $contents, string $message = ''): void
+ {
+ $this->assertThat($contents, new MailSubjectContains($at), $message);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php
new file mode 100644
index 000000000..d858fdeee
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php
@@ -0,0 +1,118 @@
+setDebug(in_array('--debug', $_SERVER['argv'], true));
+ }
+ $this->_fixtureManager = $manager;
+ $this->_fixtureManager->shutDown();
+ }
+
+ /**
+ * Iterates the tests inside a test suite and creates the required fixtures as
+ * they were expressed inside each test case.
+ *
+ * @param \PHPUnit\Framework\TestSuite $suite The test suite
+ * @return void
+ */
+ public function startTestSuite(TestSuite $suite): void
+ {
+ if (empty($this->_first)) {
+ $this->_first = $suite;
+ }
+ }
+
+ /**
+ * Destroys the fixtures created by the fixture manager at the end of the test
+ * suite run
+ *
+ * @param \PHPUnit\Framework\TestSuite $suite The test suite
+ * @return void
+ */
+ public function endTestSuite(TestSuite $suite): void
+ {
+ if ($this->_first === $suite) {
+ $this->_fixtureManager->shutDown();
+ }
+ }
+
+ /**
+ * Adds fixtures to a test case when it starts.
+ *
+ * @param \PHPUnit\Framework\Test $test The test case
+ * @return void
+ */
+ public function startTest(Test $test): void
+ {
+ /** @psalm-suppress NoInterfaceProperties */
+ $test->fixtureManager = $this->_fixtureManager;
+ if ($test instanceof TestCase) {
+ $this->_fixtureManager->fixturize($test);
+ $this->_fixtureManager->load($test);
+ }
+ }
+
+ /**
+ * Unloads fixtures from the test case.
+ *
+ * @param \PHPUnit\Framework\Test $test The test case
+ * @param float $time current time
+ * @return void
+ */
+ public function endTest(Test $test, float $time): void
+ {
+ if ($test instanceof TestCase) {
+ $this->_fixtureManager->unload($test);
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureManager.php b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureManager.php
new file mode 100644
index 000000000..e0f00874e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureManager.php
@@ -0,0 +1,515 @@
+_debug = $debug;
+ }
+
+ /**
+ * Inspects the test to look for unloaded fixtures and loads them
+ *
+ * @param \Cake\TestSuite\TestCase $test The test case to inspect.
+ * @return void
+ */
+ public function fixturize(TestCase $test): void
+ {
+ $this->_initDb();
+ if (!$test->getFixtures() || !empty($this->_processed[get_class($test)])) {
+ return;
+ }
+ $this->_loadFixtures($test);
+ $this->_processed[get_class($test)] = true;
+ }
+
+ /**
+ * Get the loaded fixtures.
+ *
+ * @return \Cake\Datasource\FixtureInterface[]
+ */
+ public function loaded(): array
+ {
+ return $this->_loaded;
+ }
+
+ /**
+ * Add aliases for all non test prefixed connections.
+ *
+ * This allows models to use the test connections without
+ * a pile of configuration work.
+ *
+ * @return void
+ */
+ protected function _aliasConnections(): void
+ {
+ $connections = ConnectionManager::configured();
+ ConnectionManager::alias('test', 'default');
+ $map = [];
+ foreach ($connections as $connection) {
+ if ($connection === 'test' || $connection === 'default') {
+ continue;
+ }
+ if (isset($map[$connection])) {
+ continue;
+ }
+ if (strpos($connection, 'test_') === 0) {
+ $map[$connection] = substr($connection, 5);
+ } else {
+ $map['test_' . $connection] = $connection;
+ }
+ }
+ foreach ($map as $testConnection => $normal) {
+ ConnectionManager::alias($testConnection, $normal);
+ }
+ }
+
+ /**
+ * Initializes this class with a DataSource object to use as default for all fixtures
+ *
+ * @return void
+ */
+ protected function _initDb(): void
+ {
+ if ($this->_initialized) {
+ return;
+ }
+ $this->_aliasConnections();
+ $this->_initialized = true;
+ }
+
+ /**
+ * Looks for fixture files and instantiates the classes accordingly
+ *
+ * @param \Cake\TestSuite\TestCase $test The test suite to load fixtures for.
+ * @return void
+ * @throws \UnexpectedValueException when a referenced fixture does not exist.
+ */
+ protected function _loadFixtures(TestCase $test): void
+ {
+ $fixtures = $test->getFixtures();
+ if (!$fixtures) {
+ return;
+ }
+ foreach ($fixtures as $fixture) {
+ if (isset($this->_loaded[$fixture])) {
+ continue;
+ }
+
+ if (strpos($fixture, '.')) {
+ [$type, $pathName] = explode('.', $fixture, 2);
+ $path = explode('/', $pathName);
+ $name = array_pop($path);
+ $additionalPath = implode('\\', $path);
+
+ if ($type === 'core') {
+ $baseNamespace = 'Cake';
+ } elseif ($type === 'app') {
+ $baseNamespace = Configure::read('App.namespace');
+ } elseif ($type === 'plugin') {
+ [$plugin, $name] = explode('.', $pathName);
+ $baseNamespace = str_replace('/', '\\', $plugin);
+ $additionalPath = null;
+ } else {
+ $baseNamespace = '';
+ $name = $fixture;
+ }
+
+ if (strpos($name, '/') > 0) {
+ $name = str_replace('/', '\\', $name);
+ }
+
+ $nameSegments = [
+ $baseNamespace,
+ 'Test\Fixture',
+ $additionalPath,
+ $name . 'Fixture',
+ ];
+ /** @psalm-var class-string<\Cake\Datasource\FixtureInterface> */
+ $className = implode('\\', array_filter($nameSegments));
+ } else {
+ /** @psalm-var class-string<\Cake\Datasource\FixtureInterface> */
+ $className = $fixture;
+ /** @psalm-suppress PossiblyFalseArgument */
+ $name = preg_replace('/Fixture\z/', '', substr(strrchr($fixture, '\\'), 1));
+ }
+
+ if (class_exists($className)) {
+ $this->_loaded[$fixture] = new $className();
+ $this->_fixtureMap[$name] = $this->_loaded[$fixture];
+ } else {
+ $msg = sprintf(
+ 'Referenced fixture class "%s" not found. Fixture "%s" was referenced in test case "%s".',
+ $className,
+ $fixture,
+ get_class($test)
+ );
+ throw new UnexpectedValueException($msg);
+ }
+ }
+ }
+
+ /**
+ * Runs the drop and create commands on the fixtures if necessary.
+ *
+ * @param \Cake\Datasource\FixtureInterface $fixture the fixture object to create
+ * @param \Cake\Datasource\ConnectionInterface $db The Connection object instance to use
+ * @param string[] $sources The existing tables in the datasource.
+ * @param bool $drop whether drop the fixture if it is already created or not
+ * @return void
+ */
+ protected function _setupTable(
+ FixtureInterface $fixture,
+ ConnectionInterface $db,
+ array $sources,
+ bool $drop = true
+ ): void {
+ $configName = $db->configName();
+ $isFixtureSetup = $this->isFixtureSetup($configName, $fixture);
+ if ($isFixtureSetup) {
+ return;
+ }
+
+ $table = $fixture->sourceName();
+ $exists = in_array($table, $sources, true);
+
+ $hasSchema = $fixture instanceof TableSchemaAwareInterface && $fixture->getTableSchema() instanceof TableSchema;
+
+ if (($drop && $exists) || ($exists && $hasSchema)) {
+ $fixture->drop($db);
+ $fixture->create($db);
+ } elseif (!$exists) {
+ $fixture->create($db);
+ } else {
+ $fixture->truncate($db);
+ }
+
+ $this->_insertionMap[$configName][] = $fixture;
+ }
+
+ /**
+ * Creates the fixtures tables and inserts data on them.
+ *
+ * @param \Cake\TestSuite\TestCase $test The test to inspect for fixture loading.
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException When fixture records cannot be inserted.
+ * @throws \RuntimeException
+ */
+ public function load(TestCase $test): void
+ {
+ $fixtures = $test->getFixtures();
+ if (!$fixtures || !$test->autoFixtures) {
+ return;
+ }
+
+ try {
+ $createTables = function (ConnectionInterface $db, array $fixtures) use ($test): void {
+ /** @var \Cake\Datasource\FixtureInterface[] $fixtures */
+ $tables = $db->getSchemaCollection()->listTables();
+ $configName = $db->configName();
+ if (!isset($this->_insertionMap[$configName])) {
+ $this->_insertionMap[$configName] = [];
+ }
+
+ foreach ($fixtures as $fixture) {
+ if (!$fixture instanceof ConstraintsInterface) {
+ continue;
+ }
+
+ if (in_array($fixture->sourceName(), $tables, true)) {
+ try {
+ $fixture->dropConstraints($db);
+ } catch (PDOException $e) {
+ $msg = sprintf(
+ 'Unable to drop constraints for fixture "%s" in "%s" test case: ' . "\n" . '%s',
+ get_class($fixture),
+ get_class($test),
+ $e->getMessage()
+ );
+ throw new CakeException($msg, null, $e);
+ }
+ }
+ }
+
+ foreach ($fixtures as $fixture) {
+ if (!in_array($fixture, $this->_insertionMap[$configName], true)) {
+ $this->_setupTable($fixture, $db, $tables, $test->dropTables);
+ } else {
+ $fixture->truncate($db);
+ }
+ }
+
+ foreach ($fixtures as $fixture) {
+ if (!$fixture instanceof ConstraintsInterface) {
+ continue;
+ }
+
+ try {
+ $fixture->createConstraints($db);
+ } catch (PDOException $e) {
+ $msg = sprintf(
+ 'Unable to create constraints for fixture "%s" in "%s" test case: ' . "\n" . '%s',
+ get_class($fixture),
+ get_class($test),
+ $e->getMessage()
+ );
+ throw new CakeException($msg, null, $e);
+ }
+ }
+ };
+ $this->_runOperation($fixtures, $createTables);
+
+ // Use a separate transaction because of postgres.
+ $insert = function (ConnectionInterface $db, array $fixtures) use ($test): void {
+ foreach ($fixtures as $fixture) {
+ try {
+ $fixture->insert($db);
+ } catch (PDOException $e) {
+ $msg = sprintf(
+ 'Unable to insert fixture "%s" in "%s" test case: ' . "\n" . '%s',
+ get_class($fixture),
+ get_class($test),
+ $e->getMessage()
+ );
+ throw new CakeException($msg, null, $e);
+ }
+ }
+ };
+ $this->_runOperation($fixtures, $insert);
+ } catch (PDOException $e) {
+ $msg = sprintf(
+ 'Unable to insert fixtures for "%s" test case. %s',
+ get_class($test),
+ $e->getMessage()
+ );
+ throw new RuntimeException($msg, 0, $e);
+ }
+ }
+
+ /**
+ * Run a function on each connection and collection of fixtures.
+ *
+ * @param string[] $fixtures A list of fixtures to operate on.
+ * @param callable $operation The operation to run on each connection + fixture set.
+ * @return void
+ */
+ protected function _runOperation(array $fixtures, callable $operation): void
+ {
+ $dbs = $this->_fixtureConnections($fixtures);
+ foreach ($dbs as $connection => $fixtures) {
+ $db = ConnectionManager::get($connection);
+ $logQueries = $db->isQueryLoggingEnabled();
+
+ if ($logQueries && !$this->_debug) {
+ $db->disableQueryLogging();
+ }
+ $db->transactional(function (ConnectionInterface $db) use ($fixtures, $operation): void {
+ $db->disableConstraints(function (ConnectionInterface $db) use ($fixtures, $operation): void {
+ $operation($db, $fixtures);
+ });
+ });
+ if ($logQueries) {
+ $db->enableQueryLogging(true);
+ }
+ }
+ }
+
+ /**
+ * Get the unique list of connections that a set of fixtures contains.
+ *
+ * @param string[] $fixtures The array of fixtures a list of connections is needed from.
+ * @return array An array of connection names.
+ */
+ protected function _fixtureConnections(array $fixtures): array
+ {
+ $dbs = [];
+ foreach ($fixtures as $name) {
+ if (!empty($this->_loaded[$name])) {
+ $fixture = $this->_loaded[$name];
+ $dbs[$fixture->connection()][$name] = $fixture;
+ }
+ }
+
+ return $dbs;
+ }
+
+ /**
+ * Truncates the fixtures tables
+ *
+ * @param \Cake\TestSuite\TestCase $test The test to inspect for fixture unloading.
+ * @return void
+ */
+ public function unload(TestCase $test): void
+ {
+ $fixtures = $test->getFixtures();
+ if (!$fixtures) {
+ return;
+ }
+ $truncate = function (ConnectionInterface $db, array $fixtures): void {
+ $configName = $db->configName();
+
+ foreach ($fixtures as $name => $fixture) {
+ if (
+ $this->isFixtureSetup($configName, $fixture)
+ && $fixture instanceof ConstraintsInterface
+ ) {
+ $fixture->dropConstraints($db);
+ }
+ }
+ };
+ $this->_runOperation($fixtures, $truncate);
+ }
+
+ /**
+ * Creates a single fixture table and loads data into it.
+ *
+ * @param string $name of the fixture
+ * @param \Cake\Datasource\ConnectionInterface|null $connection Connection instance or null
+ * to get a Connection from the fixture.
+ * @param bool $dropTables Whether or not tables should be dropped and re-created.
+ * @return void
+ * @throws \UnexpectedValueException if $name is not a previously loaded class
+ */
+ public function loadSingle(string $name, ?ConnectionInterface $connection = null, bool $dropTables = true): void
+ {
+ if (!isset($this->_fixtureMap[$name])) {
+ throw new UnexpectedValueException(sprintf('Referenced fixture class %s not found', $name));
+ }
+
+ $fixture = $this->_fixtureMap[$name];
+ if (!$connection) {
+ $connection = ConnectionManager::get($fixture->connection());
+ }
+
+ if (!$this->isFixtureSetup($connection->configName(), $fixture)) {
+ $sources = $connection->getSchemaCollection()->listTables();
+ $this->_setupTable($fixture, $connection, $sources, $dropTables);
+ }
+
+ if (!$dropTables) {
+ if ($fixture instanceof ConstraintsInterface) {
+ $fixture->dropConstraints($connection);
+ }
+ $fixture->truncate($connection);
+ }
+
+ if ($fixture instanceof ConstraintsInterface) {
+ $fixture->createConstraints($connection);
+ }
+ $fixture->insert($connection);
+ }
+
+ /**
+ * Drop all fixture tables loaded by this class
+ *
+ * @return void
+ */
+ public function shutDown(): void
+ {
+ $shutdown = function (ConnectionInterface $db, array $fixtures): void {
+ $connection = $db->configName();
+ /** @var \Cake\Datasource\FixtureInterface $fixture */
+ foreach ($fixtures as $fixture) {
+ if ($this->isFixtureSetup($connection, $fixture)) {
+ $fixture->drop($db);
+ $index = array_search($fixture, $this->_insertionMap[$connection], true);
+ unset($this->_insertionMap[$connection][$index]);
+ }
+ }
+ };
+ $this->_runOperation(array_keys($this->_loaded), $shutdown);
+ }
+
+ /**
+ * Check whether or not a fixture has been inserted in a given connection name.
+ *
+ * @param string $connection The connection name.
+ * @param \Cake\Datasource\FixtureInterface $fixture The fixture to check.
+ * @return bool
+ */
+ public function isFixtureSetup(string $connection, FixtureInterface $fixture): bool
+ {
+ return isset($this->_insertionMap[$connection]) && in_array($fixture, $this->_insertionMap[$connection], true);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php
new file mode 100644
index 000000000..f51277028
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Fixture/TestFixture.php
@@ -0,0 +1,464 @@
+connection)) {
+ $connection = $this->connection;
+ if (strpos($connection, 'test') !== 0) {
+ $message = sprintf(
+ 'Invalid datasource name "%s" for "%s" fixture. Fixture datasource names must begin with "test".',
+ $connection,
+ static::class
+ );
+ throw new CakeException($message);
+ }
+ }
+ $this->init();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function connection(): string
+ {
+ return $this->connection;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function sourceName(): string
+ {
+ return $this->table;
+ }
+
+ /**
+ * Initialize the fixture.
+ *
+ * @return void
+ * @throws \Cake\ORM\Exception\MissingTableClassException When importing from a table that does not exist.
+ */
+ public function init(): void
+ {
+ if ($this->table === null) {
+ $this->table = $this->_tableFromClass();
+ }
+
+ if (empty($this->import) && !empty($this->fields)) {
+ $this->_schemaFromFields();
+ }
+
+ if (!empty($this->import)) {
+ $this->_schemaFromImport();
+ }
+
+ if (empty($this->import) && empty($this->fields)) {
+ $this->_schemaFromReflection();
+ }
+ }
+
+ /**
+ * Returns the table name using the fixture class
+ *
+ * @return string
+ */
+ protected function _tableFromClass(): string
+ {
+ [, $class] = namespaceSplit(static::class);
+ preg_match('/^(.*)Fixture$/', $class, $matches);
+ $table = $class;
+
+ if (isset($matches[1])) {
+ $table = $matches[1];
+ }
+
+ return Inflector::tableize($table);
+ }
+
+ /**
+ * Build the fixtures table schema from the fields property.
+ *
+ * @return void
+ */
+ protected function _schemaFromFields(): void
+ {
+ $connection = ConnectionManager::get($this->connection());
+ $this->_schema = $connection->getDriver()->newTableSchema($this->table);
+ foreach ($this->fields as $field => $data) {
+ if ($field === '_constraints' || $field === '_indexes' || $field === '_options') {
+ continue;
+ }
+ $this->_schema->addColumn($field, $data);
+ }
+ if (!empty($this->fields['_constraints'])) {
+ foreach ($this->fields['_constraints'] as $name => $data) {
+ if (!$connection->supportsDynamicConstraints() || $data['type'] !== TableSchema::CONSTRAINT_FOREIGN) {
+ $this->_schema->addConstraint($name, $data);
+ } else {
+ $this->_constraints[$name] = $data;
+ }
+ }
+ }
+ if (!empty($this->fields['_indexes'])) {
+ foreach ($this->fields['_indexes'] as $name => $data) {
+ $this->_schema->addIndex($name, $data);
+ }
+ }
+ if (!empty($this->fields['_options'])) {
+ $this->_schema->setOptions($this->fields['_options']);
+ }
+ }
+
+ /**
+ * Build fixture schema from a table in another datasource.
+ *
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException when trying to import from an empty table.
+ */
+ protected function _schemaFromImport(): void
+ {
+ if (!is_array($this->import)) {
+ return;
+ }
+ $import = $this->import + ['connection' => 'default', 'table' => null, 'model' => null];
+
+ if (!empty($import['model'])) {
+ if (!empty($import['table'])) {
+ throw new CakeException('You cannot define both table and model.');
+ }
+ $import['table'] = $this->getTableLocator()->get($import['model'])->getTable();
+ }
+
+ if (empty($import['table'])) {
+ throw new CakeException('Cannot import from undefined table.');
+ }
+
+ $this->table = $import['table'];
+
+ $db = ConnectionManager::get($import['connection'], false);
+ $schemaCollection = $db->getSchemaCollection();
+ $table = $schemaCollection->describe($import['table']);
+ $this->_schema = $table;
+ }
+
+ /**
+ * Build fixture schema directly from the datasource
+ *
+ * @return void
+ * @throws \Cake\Core\Exception\CakeException when trying to reflect a table that does not exist
+ */
+ protected function _schemaFromReflection(): void
+ {
+ $db = ConnectionManager::get($this->connection());
+ $schemaCollection = $db->getSchemaCollection();
+ $tables = $schemaCollection->listTables();
+
+ if (!in_array($this->table, $tables, true)) {
+ throw new CakeException(
+ sprintf(
+ 'Cannot describe schema for table `%s` for fixture `%s`: the table does not exist.',
+ $this->table,
+ static::class
+ )
+ );
+ }
+
+ $this->_schema = $schemaCollection->describe($this->table);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function create(ConnectionInterface $connection): bool
+ {
+ if (empty($this->_schema)) {
+ return false;
+ }
+
+ if (empty($this->import) && empty($this->fields)) {
+ return true;
+ }
+
+ try {
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $queries = $this->_schema->createSql($connection);
+ foreach ($queries as $query) {
+ $stmt = $connection->prepare($query);
+ $stmt->execute();
+ $stmt->closeCursor();
+ }
+ } catch (Exception $e) {
+ $msg = sprintf(
+ 'Fixture creation for "%s" failed "%s"',
+ $this->table,
+ $e->getMessage()
+ );
+ Log::error($msg);
+ trigger_error($msg, E_USER_WARNING);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function drop(ConnectionInterface $connection): bool
+ {
+ if (empty($this->_schema)) {
+ return false;
+ }
+
+ if (empty($this->import) && empty($this->fields)) {
+ return $this->truncate($connection);
+ }
+
+ try {
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $sql = $this->_schema->dropSql($connection);
+ foreach ($sql as $stmt) {
+ $connection->execute($stmt)->closeCursor();
+ }
+ } catch (Exception $e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function insert(ConnectionInterface $connection)
+ {
+ if (isset($this->records) && !empty($this->records)) {
+ [$fields, $values, $types] = $this->_getRecords();
+ $query = $connection->newQuery()
+ ->insert($fields, $types)
+ ->into($this->sourceName());
+
+ foreach ($values as $row) {
+ $query->values($row);
+ }
+ $statement = $query->execute();
+ $statement->closeCursor();
+
+ return $statement;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createConstraints(ConnectionInterface $connection): bool
+ {
+ if (empty($this->_constraints)) {
+ return true;
+ }
+
+ foreach ($this->_constraints as $name => $data) {
+ $this->_schema->addConstraint($name, $data);
+ }
+
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $sql = $this->_schema->addConstraintSql($connection);
+
+ if (empty($sql)) {
+ return true;
+ }
+
+ foreach ($sql as $stmt) {
+ $connection->execute($stmt)->closeCursor();
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dropConstraints(ConnectionInterface $connection): bool
+ {
+ if (empty($this->_constraints)) {
+ return true;
+ }
+
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $sql = $this->_schema->dropConstraintSql($connection);
+
+ if (empty($sql)) {
+ return true;
+ }
+
+ foreach ($sql as $stmt) {
+ $connection->execute($stmt)->closeCursor();
+ }
+
+ foreach ($this->_constraints as $name => $data) {
+ $this->_schema->dropConstraint($name);
+ }
+
+ return true;
+ }
+
+ /**
+ * Converts the internal records into data used to generate a query.
+ *
+ * @return array
+ */
+ protected function _getRecords(): array
+ {
+ $fields = $values = $types = [];
+ $columns = $this->_schema->columns();
+ foreach ($this->records as $record) {
+ $fields = array_merge($fields, array_intersect(array_keys($record), $columns));
+ }
+ $fields = array_values(array_unique($fields));
+ foreach ($fields as $field) {
+ /** @var array $column */
+ $column = $this->_schema->getColumn($field);
+ $types[$field] = $column['type'];
+ }
+ $default = array_fill_keys($fields, null);
+ foreach ($this->records as $record) {
+ $values[] = array_merge($default, $record);
+ }
+
+ return [$fields, $values, $types];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function truncate(ConnectionInterface $connection): bool
+ {
+ /** @psalm-suppress ArgumentTypeCoercion */
+ $sql = $this->_schema->truncateSql($connection);
+ foreach ($sql as $stmt) {
+ $connection->execute($stmt)->closeCursor();
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTableSchema()
+ {
+ return $this->_schema;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setTableSchema($schema)
+ {
+ $this->_schema = $schema;
+
+ return $this;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php b/app/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php
new file mode 100644
index 000000000..be0cfb251
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestCase.php
@@ -0,0 +1,43 @@
+_request = [];
+ $this->_session = [];
+ $this->_cookie = [];
+ $this->_response = null;
+ $this->_exception = null;
+ $this->_controller = null;
+ $this->_viewName = null;
+ $this->_layoutName = null;
+ $this->_requestSession = null;
+ $this->_securityToken = false;
+ $this->_csrfToken = false;
+ $this->_retainFlashMessages = false;
+ $this->_flashMessages = [];
+ }
+
+ /**
+ * Calling this method will enable a SecurityComponent
+ * compatible token to be added to request data. This
+ * lets you easily test actions protected by SecurityComponent.
+ *
+ * @return void
+ */
+ public function enableSecurityToken(): void
+ {
+ $this->_securityToken = true;
+ }
+
+ /**
+ * Set list of fields that are excluded from field validation.
+ *
+ * @param string[] $unlockedFields List of fields that are excluded from field validation.
+ * @return void
+ */
+ public function setUnlockedFields(array $unlockedFields = []): void
+ {
+ $this->_unlockedFields = $unlockedFields;
+ }
+
+ /**
+ * Calling this method will add a CSRF token to the request.
+ *
+ * Both the POST data and cookie will be populated when this option
+ * is enabled. The default parameter names will be used.
+ *
+ * @return void
+ */
+ public function enableCsrfToken(): void
+ {
+ $this->_csrfToken = true;
+ }
+
+ /**
+ * Calling this method will re-store flash messages into the test session
+ * after being removed by the FlashHelper
+ *
+ * @return void
+ */
+ public function enableRetainFlashMessages(): void
+ {
+ $this->_retainFlashMessages = true;
+ }
+
+ /**
+ * Configures the data for the *next* request.
+ *
+ * This data is cleared in the tearDown() method.
+ *
+ * You can call this method multiple times to append into
+ * the current state.
+ *
+ * @param array $data The request data to use.
+ * @return void
+ */
+ public function configRequest(array $data): void
+ {
+ $this->_request = $data + $this->_request;
+ }
+
+ /**
+ * Sets session data.
+ *
+ * This method lets you configure the session data
+ * you want to be used for requests that follow. The session
+ * state is reset in each tearDown().
+ *
+ * You can call this method multiple times to append into
+ * the current state.
+ *
+ * @param array $data The session data to use.
+ * @return void
+ */
+ public function session(array $data): void
+ {
+ $this->_session = $data + $this->_session;
+ }
+
+ /**
+ * Sets a request cookie for future requests.
+ *
+ * This method lets you configure the session data
+ * you want to be used for requests that follow. The session
+ * state is reset in each tearDown().
+ *
+ * You can call this method multiple times to append into
+ * the current state.
+ *
+ * @param string $name The cookie name to use.
+ * @param mixed $value The value of the cookie.
+ * @return void
+ */
+ public function cookie(string $name, $value): void
+ {
+ $this->_cookie[$name] = $value;
+ }
+
+ /**
+ * Returns the encryption key to be used.
+ *
+ * @return string
+ */
+ protected function _getCookieEncryptionKey(): string
+ {
+ if (isset($this->_cookieEncryptionKey)) {
+ return $this->_cookieEncryptionKey;
+ }
+
+ return Security::getSalt();
+ }
+
+ /**
+ * Sets a encrypted request cookie for future requests.
+ *
+ * The difference from cookie() is this encrypts the cookie
+ * value like the CookieComponent.
+ *
+ * @param string $name The cookie name to use.
+ * @param mixed $value The value of the cookie.
+ * @param string|false $encrypt Encryption mode to use.
+ * @param string|null $key Encryption key used. Defaults
+ * to Security.salt.
+ * @return void
+ * @see \Cake\Utility\CookieCryptTrait::_encrypt()
+ */
+ public function cookieEncrypted(string $name, $value, $encrypt = 'aes', $key = null): void
+ {
+ $this->_cookieEncryptionKey = $key;
+ $this->_cookie[$name] = $this->_encrypt($value, $encrypt);
+ }
+
+ /**
+ * Performs a GET request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @return void
+ */
+ public function get($url): void
+ {
+ $this->_sendRequest($url, 'GET');
+ }
+
+ /**
+ * Performs a POST request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @param string|array $data The data for the request.
+ * @return void
+ */
+ public function post($url, $data = []): void
+ {
+ $this->_sendRequest($url, 'POST', $data);
+ }
+
+ /**
+ * Performs a PATCH request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @param string|array $data The data for the request.
+ * @return void
+ */
+ public function patch($url, $data = []): void
+ {
+ $this->_sendRequest($url, 'PATCH', $data);
+ }
+
+ /**
+ * Performs a PUT request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @param string|array $data The data for the request.
+ * @return void
+ */
+ public function put($url, $data = []): void
+ {
+ $this->_sendRequest($url, 'PUT', $data);
+ }
+
+ /**
+ * Performs a DELETE request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @return void
+ */
+ public function delete($url): void
+ {
+ $this->_sendRequest($url, 'DELETE');
+ }
+
+ /**
+ * Performs a HEAD request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @return void
+ */
+ public function head($url): void
+ {
+ $this->_sendRequest($url, 'HEAD');
+ }
+
+ /**
+ * Performs an OPTIONS request using the current request data.
+ *
+ * The response of the dispatched request will be stored as
+ * a property. You can use various assert methods to check the
+ * response.
+ *
+ * @param string|array $url The URL to request.
+ * @return void
+ */
+ public function options($url): void
+ {
+ $this->_sendRequest($url, 'OPTIONS');
+ }
+
+ /**
+ * Creates and send the request into a Dispatcher instance.
+ *
+ * Receives and stores the response for future inspection.
+ *
+ * @param string|array $url The URL
+ * @param string $method The HTTP method
+ * @param string|array $data The request data.
+ * @return void
+ * @throws \PHPUnit\Exception|\Throwable
+ */
+ protected function _sendRequest($url, $method, $data = []): void
+ {
+ $dispatcher = $this->_makeDispatcher();
+ $url = $dispatcher->resolveUrl($url);
+
+ try {
+ $request = $this->_buildRequest($url, $method, $data);
+ $response = $dispatcher->execute($request);
+ $this->_requestSession = $request['session'];
+ if ($this->_retainFlashMessages && $this->_flashMessages) {
+ $this->_requestSession->write('Flash', $this->_flashMessages);
+ }
+ $this->_response = $response;
+ } catch (PHPUnitException | DatabaseException $e) {
+ throw $e;
+ } catch (Throwable $e) {
+ $this->_exception = $e;
+ // Simulate the global exception handler being invoked.
+ $this->_handleError($e);
+ }
+ }
+
+ /**
+ * Get the correct dispatcher instance.
+ *
+ * @return \Cake\TestSuite\MiddlewareDispatcher A dispatcher instance
+ */
+ protected function _makeDispatcher(): MiddlewareDispatcher
+ {
+ EventManager::instance()->on('Controller.initialize', [$this, 'controllerSpy']);
+ /** @var \Cake\Core\HttpApplicationInterface $app */
+ $app = $this->createApp();
+
+ return new MiddlewareDispatcher($app);
+ }
+
+ /**
+ * Adds additional event spies to the controller/view event manager.
+ *
+ * @param \Cake\Event\EventInterface $event A dispatcher event.
+ * @param \Cake\Controller\Controller|null $controller Controller instance.
+ * @return void
+ */
+ public function controllerSpy(EventInterface $event, ?Controller $controller = null): void
+ {
+ if (!$controller) {
+ /** @var \Cake\Controller\Controller $controller */
+ $controller = $event->getSubject();
+ }
+ $this->_controller = $controller;
+ $events = $controller->getEventManager();
+ $flashCapture = function (EventInterface $event): void {
+ if (!$this->_retainFlashMessages) {
+ return;
+ }
+ $controller = $event->getSubject();
+ $this->_flashMessages = Hash::merge(
+ $this->_flashMessages,
+ $controller->getRequest()->getSession()->read('Flash')
+ );
+ };
+ $events->on('Controller.beforeRedirect', ['priority' => -100], $flashCapture);
+ $events->on('Controller.beforeRender', ['priority' => -100], $flashCapture);
+ $events->on('View.beforeRender', function ($event, $viewFile): void {
+ if (!$this->_viewName) {
+ $this->_viewName = $viewFile;
+ }
+ });
+ $events->on('View.beforeLayout', function ($event, $viewFile): void {
+ $this->_layoutName = $viewFile;
+ });
+ }
+
+ /**
+ * Attempts to render an error response for a given exception.
+ *
+ * This method will attempt to use the configured exception renderer.
+ * If that class does not exist, the built-in renderer will be used.
+ *
+ * @param \Throwable $exception Exception to handle.
+ * @return void
+ */
+ protected function _handleError(Throwable $exception): void
+ {
+ $class = Configure::read('Error.exceptionRenderer');
+ if (empty($class) || !class_exists($class)) {
+ $class = ExceptionRenderer::class;
+ }
+ /** @var \Cake\Error\ExceptionRenderer $instance */
+ $instance = new $class($exception);
+ $this->_response = $instance->render();
+ }
+
+ /**
+ * Creates a request object with the configured options and parameters.
+ *
+ * @param string $url The URL
+ * @param string $method The HTTP method
+ * @param string|array $data The request data.
+ * @return array The request context
+ */
+ protected function _buildRequest(string $url, $method, $data = []): array
+ {
+ $sessionConfig = (array)Configure::read('Session') + [
+ 'defaults' => 'php',
+ ];
+ $session = Session::create($sessionConfig);
+ [$url, $query, $hostInfo] = $this->_url($url);
+ $tokenUrl = $url;
+
+ if ($query) {
+ $tokenUrl .= '?' . $query;
+ }
+
+ parse_str($query, $queryData);
+
+ $env = [
+ 'REQUEST_METHOD' => $method,
+ 'QUERY_STRING' => $query,
+ 'REQUEST_URI' => $url,
+ ];
+ if (!empty($hostInfo['ssl'])) {
+ $env['HTTPS'] = 'on';
+ }
+ if (isset($hostInfo['host'])) {
+ $env['HTTP_HOST'] = $hostInfo['host'];
+ }
+ if (isset($this->_request['headers'])) {
+ foreach ($this->_request['headers'] as $k => $v) {
+ $name = strtoupper(str_replace('-', '_', $k));
+ if (!in_array($name, ['CONTENT_LENGTH', 'CONTENT_TYPE'], true)) {
+ $name = 'HTTP_' . $name;
+ }
+ $env[$name] = $v;
+ }
+ unset($this->_request['headers']);
+ }
+ $props = [
+ 'url' => $url,
+ 'session' => $session,
+ 'query' => $queryData,
+ 'files' => [],
+ 'environment' => $env,
+ ];
+
+ if (is_string($data)) {
+ $props['input'] = $data;
+ } elseif (
+ is_array($data) &&
+ isset($props['environment']['CONTENT_TYPE']) &&
+ $props['environment']['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'
+ ) {
+ $props['input'] = http_build_query($data);
+ } else {
+ $data = $this->_addTokens($tokenUrl, $data);
+ $props['post'] = $this->_castToString($data);
+ }
+
+ $props['cookies'] = $this->_cookie;
+ $session->write($this->_session);
+ $props = Hash::merge($props, $this->_request);
+
+ return $props;
+ }
+
+ /**
+ * Add the CSRF and Security Component tokens if necessary.
+ *
+ * @param string $url The URL the form is being submitted on.
+ * @param array $data The request body data.
+ * @return array The request body with tokens added.
+ */
+ protected function _addTokens(string $url, array $data): array
+ {
+ if ($this->_securityToken === true) {
+ $fields = array_diff_key($data, array_flip($this->_unlockedFields));
+
+ $keys = array_map(function ($field) {
+ return preg_replace('/(\.\d+)+$/', '', $field);
+ }, array_keys(Hash::flatten($fields)));
+
+ $formProtector = new FormProtector(['unlockedFields' => $this->_unlockedFields]);
+ foreach ($keys as $field) {
+ $formProtector->addField($field);
+ }
+ $tokenData = $formProtector->buildTokenData($url, 'cli');
+
+ $data['_Token'] = $tokenData;
+ $data['_Token']['debug'] = 'FormProtector debug data would be added here';
+ }
+
+ if ($this->_csrfToken === true) {
+ $middleware = new CsrfProtectionMiddleware();
+ $token = null;
+ if (!isset($this->_cookie['csrfToken']) && !isset($this->_session['csrfToken'])) {
+ $token = $middleware->createToken();
+ } elseif (isset($this->_cookie['csrfToken'])) {
+ $token = $this->_cookie['csrfToken'];
+ } else {
+ $token = $this->_session['csrfToken'];
+ }
+
+ // Add the token to both the session and cookie to cover
+ // both types of CSRF tokens. We generate the token with the cookie
+ // middleware as cookie tokens will be accepted by session csrf, but not
+ // the inverse.
+ $this->_session['csrfToken'] = $token;
+ $this->_cookie['csrfToken'] = $token;
+ if (!isset($data['_csrfToken'])) {
+ $data['_csrfToken'] = $token;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Recursively casts all data to string as that is how data would be POSTed in
+ * the real world
+ *
+ * @param array $data POST data
+ * @return array
+ */
+ protected function _castToString(array $data): array
+ {
+ foreach ($data as $key => $value) {
+ if (is_scalar($value)) {
+ $data[$key] = $value === false ? '0' : (string)$value;
+
+ continue;
+ }
+
+ if (is_array($value)) {
+ $looksLikeFile = isset($value['error'], $value['tmp_name'], $value['size']);
+ if ($looksLikeFile) {
+ continue;
+ }
+
+ $data[$key] = $this->_castToString($value);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Creates a valid request url and parameter array more like Request::_url()
+ *
+ * @param string $url The URL
+ * @return array Qualified URL, the query parameters, and host data
+ */
+ protected function _url(string $url): array
+ {
+ $uri = new Uri($url);
+ $path = $uri->getPath();
+ $query = $uri->getQuery();
+
+ $hostData = [];
+ if ($uri->getHost()) {
+ $hostData['host'] = $uri->getHost();
+ }
+ if ($uri->getScheme()) {
+ $hostData['ssl'] = $uri->getScheme() === 'https';
+ }
+
+ return [$path, $query, $hostData];
+ }
+
+ /**
+ * Get the response body as string
+ *
+ * @return string The response body.
+ */
+ protected function _getBodyAsString(): string
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert content.');
+ }
+
+ return (string)$this->_response->getBody();
+ }
+
+ /**
+ * Fetches a view variable by name.
+ *
+ * If the view variable does not exist, null will be returned.
+ *
+ * @param string $name The view variable to get.
+ * @return mixed The view variable if set.
+ */
+ public function viewVariable(string $name)
+ {
+ return $this->_controller ? $this->_controller->viewBuilder()->getVar($name) : null;
+ }
+
+ /**
+ * Asserts that the response status code is in the 2xx range.
+ *
+ * @param string $message Custom message for failure.
+ * @return void
+ */
+ public function assertResponseOk(string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new StatusOk($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the response status code is in the 2xx/3xx range.
+ *
+ * @param string $message Custom message for failure.
+ * @return void
+ */
+ public function assertResponseSuccess(string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new StatusSuccess($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the response status code is in the 4xx range.
+ *
+ * @param string $message Custom message for failure.
+ * @return void
+ */
+ public function assertResponseError(string $message = ''): void
+ {
+ $this->assertThat(null, new StatusError($this->_response), $message);
+ }
+
+ /**
+ * Asserts that the response status code is in the 5xx range.
+ *
+ * @param string $message Custom message for failure.
+ * @return void
+ */
+ public function assertResponseFailure(string $message = ''): void
+ {
+ $this->assertThat(null, new StatusFailure($this->_response), $message);
+ }
+
+ /**
+ * Asserts a specific response status code.
+ *
+ * @param int $code Status code to assert.
+ * @param string $message Custom message for failure.
+ * @return void
+ */
+ public function assertResponseCode(int $code, string $message = ''): void
+ {
+ $this->assertThat($code, new StatusCode($this->_response), $message);
+ }
+
+ /**
+ * Asserts that the Location header is correct. Comparison is made against a full URL.
+ *
+ * @param string|array|null $url The URL you expected the client to go to. This
+ * can either be a string URL or an array compatible with Router::url(). Use null to
+ * simply check for the existence of this header.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertRedirect($url = null, $message = ''): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage);
+
+ if ($url) {
+ $this->assertThat(
+ Router::url($url, true),
+ new HeaderEquals($this->_response, 'Location'),
+ $verboseMessage
+ );
+ }
+ }
+
+ /**
+ * Asserts that the Location header is correct. Comparison is made against exactly the URL provided.
+ *
+ * @param string|array|null $url The URL you expected the client to go to. This
+ * can either be a string URL or an array compatible with Router::url(). Use null to
+ * simply check for the existence of this header.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertRedirectEquals($url = null, $message = '')
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage);
+
+ if ($url) {
+ $this->assertThat(Router::url($url), new HeaderEquals($this->_response, 'Location'), $verboseMessage);
+ }
+ }
+
+ /**
+ * Asserts that the Location header contains a substring
+ *
+ * @param string $url The URL you expected the client to go to.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertRedirectContains(string $url, string $message = ''): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage);
+ $this->assertThat($url, new HeaderContains($this->_response, 'Location'), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the Location header does not contain a substring
+ *
+ * @param string $url The URL you expected the client to go to.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertRedirectNotContains(string $url, string $message = ''): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, 'Location'), $verboseMessage);
+ $this->assertThat($url, new HeaderNotContains($this->_response, 'Location'), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the Location header is not set.
+ *
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertNoRedirect(string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderNotSet($this->_response, 'Location'), $verboseMessage);
+ }
+
+ /**
+ * Asserts response headers
+ *
+ * @param string $header The header to check
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertHeader(string $header, string $content, string $message = ''): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, $header), $verboseMessage);
+ $this->assertThat($content, new HeaderEquals($this->_response, $header), $verboseMessage);
+ }
+
+ /**
+ * Asserts response header contains a string
+ *
+ * @param string $header The header to check
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertHeaderContains(string $header, string $content, string $message = ''): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, $header), $verboseMessage);
+ $this->assertThat($content, new HeaderContains($this->_response, $header), $verboseMessage);
+ }
+
+ /**
+ * Asserts response header does not contain a string
+ *
+ * @param string $header The header to check
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertHeaderNotContains(string $header, string $content, string $message = ''): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert header.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new HeaderSet($this->_response, $header), $verboseMessage);
+ $this->assertThat($content, new HeaderNotContains($this->_response, $header), $verboseMessage);
+ }
+
+ /**
+ * Asserts content type
+ *
+ * @param string $type The content-type to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertContentType(string $type, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($type, new ContentType($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Asserts content in the response body equals.
+ *
+ * @param mixed $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertResponseEquals($content, $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($content, new BodyEquals($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Asserts content in the response body not equals.
+ *
+ * @param mixed $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertResponseNotEquals($content, $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($content, new BodyNotEquals($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Asserts content exists in the response body.
+ *
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @param bool $ignoreCase A flag to check whether we should ignore case or not.
+ * @return void
+ */
+ public function assertResponseContains(string $content, string $message = '', bool $ignoreCase = false): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert content.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($content, new BodyContains($this->_response, $ignoreCase), $verboseMessage);
+ }
+
+ /**
+ * Asserts content does not exist in the response body.
+ *
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @param bool $ignoreCase A flag to check whether we should ignore case or not.
+ * @return void
+ */
+ public function assertResponseNotContains(string $content, string $message = '', bool $ignoreCase = false): void
+ {
+ if (!$this->_response) {
+ $this->fail('No response set, cannot assert content.');
+ }
+
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($content, new BodyNotContains($this->_response, $ignoreCase), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the response body matches a given regular expression.
+ *
+ * @param string $pattern The pattern to compare against.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertResponseRegExp(string $pattern, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($pattern, new BodyRegExp($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the response body does not match a given regular expression.
+ *
+ * @param string $pattern The pattern to compare against.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertResponseNotRegExp(string $pattern, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($pattern, new BodyNotRegExp($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Assert response content is not empty.
+ *
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertResponseNotEmpty(string $message = ''): void
+ {
+ $this->assertThat(null, new BodyNotEmpty($this->_response), $message);
+ }
+
+ /**
+ * Assert response content is empty.
+ *
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertResponseEmpty(string $message = ''): void
+ {
+ $this->assertThat(null, new BodyEmpty($this->_response), $message);
+ }
+
+ /**
+ * Asserts that the search string was in the template name.
+ *
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertTemplate(string $content, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($content, new TemplateFileEquals($this->_viewName), $verboseMessage);
+ }
+
+ /**
+ * Asserts that the search string was in the layout name.
+ *
+ * @param string $content The content to check for.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertLayout(string $content, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($content, new LayoutFileEquals($this->_layoutName), $verboseMessage);
+ }
+
+ /**
+ * Asserts session contents
+ *
+ * @param mixed $expected The expected contents.
+ * @param string $path The session data path. Uses Hash::get() compatible notation
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertSession($expected, string $path, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($expected, new SessionEquals($path), $verboseMessage);
+ }
+
+ /**
+ * Asserts session key exists.
+ *
+ * @param string $path The session data path. Uses Hash::get() compatible notation.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertSessionHasKey(string $path, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($path, new SessionHasKey($path), $verboseMessage);
+ }
+
+ /**
+ * Asserts a session key does not exist.
+ *
+ * @param string $path The session data path. Uses Hash::get() compatible notation.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertSessionNotHasKey(string $path, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($path, $this->logicalNot(new SessionHasKey($path)), $verboseMessage);
+ }
+
+ /**
+ * Asserts a flash message was set
+ *
+ * @param string $expected Expected message
+ * @param string $key Flash key
+ * @param string $message Assertion failure message
+ * @return void
+ */
+ public function assertFlashMessage(string $expected, string $key = 'flash', string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($expected, new FlashParamEquals($this->_requestSession, $key, 'message'), $verboseMessage);
+ }
+
+ /**
+ * Asserts a flash message was set at a certain index
+ *
+ * @param int $at Flash index
+ * @param string $expected Expected message
+ * @param string $key Flash key
+ * @param string $message Assertion failure message
+ * @return void
+ */
+ public function assertFlashMessageAt(int $at, string $expected, string $key = 'flash', string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(
+ $expected,
+ new FlashParamEquals($this->_requestSession, $key, 'message', $at),
+ $verboseMessage
+ );
+ }
+
+ /**
+ * Asserts a flash element was set
+ *
+ * @param string $expected Expected element name
+ * @param string $key Flash key
+ * @param string $message Assertion failure message
+ * @return void
+ */
+ public function assertFlashElement(string $expected, string $key = 'flash', string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(
+ $expected,
+ new FlashParamEquals($this->_requestSession, $key, 'element'),
+ $verboseMessage
+ );
+ }
+
+ /**
+ * Asserts a flash element was set at a certain index
+ *
+ * @param int $at Flash index
+ * @param string $expected Expected element name
+ * @param string $key Flash key
+ * @param string $message Assertion failure message
+ * @return void
+ */
+ public function assertFlashElementAt(int $at, string $expected, string $key = 'flash', string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(
+ $expected,
+ new FlashParamEquals($this->_requestSession, $key, 'element', $at),
+ $verboseMessage
+ );
+ }
+
+ /**
+ * Asserts cookie values
+ *
+ * @param mixed $expected The expected contents.
+ * @param string $name The cookie name.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertCookie($expected, string $name, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($name, new CookieSet($this->_response), $verboseMessage);
+ $this->assertThat($expected, new CookieEquals($this->_response, $name), $verboseMessage);
+ }
+
+ /**
+ * Asserts a cookie has not been set in the response
+ *
+ * @param string $cookie The cookie name to check
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertCookieNotSet(string $cookie, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($cookie, new CookieNotSet($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Disable the error handler middleware.
+ *
+ * By using this function, exceptions are no longer caught by the ErrorHandlerMiddleware
+ * and are instead re-thrown by the TestExceptionRenderer. This can be helpful
+ * when trying to diagnose/debug unexpected failures in test cases.
+ *
+ * @return void
+ */
+ public function disableErrorHandlerMiddleware(): void
+ {
+ Configure::write('Error.exceptionRenderer', TestExceptionRenderer::class);
+ }
+
+ /**
+ * Asserts cookie values which are encrypted by the
+ * CookieComponent.
+ *
+ * The difference from assertCookie() is this decrypts the cookie
+ * value like the CookieComponent for this assertion.
+ *
+ * @param mixed $expected The expected contents.
+ * @param string $name The cookie name.
+ * @param string $encrypt Encryption mode to use.
+ * @param string|null $key Encryption key used. Defaults
+ * to Security.salt.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ * @see \Cake\Utility\CookieCryptTrait::_encrypt()
+ */
+ public function assertCookieEncrypted(
+ $expected,
+ string $name,
+ string $encrypt = 'aes',
+ ?string $key = null,
+ string $message = ''
+ ): void {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat($name, new CookieSet($this->_response), $verboseMessage);
+
+ $this->_cookieEncryptionKey = $key;
+ $this->assertThat(
+ $expected,
+ new CookieEncryptedEquals($this->_response, $name, $encrypt, $this->_getCookieEncryptionKey())
+ );
+ }
+
+ /**
+ * Asserts that a file with the given name was sent in the response
+ *
+ * @param string $expected The absolute file path that should be sent in the response.
+ * @param string $message The failure message that will be appended to the generated message.
+ * @return void
+ */
+ public function assertFileResponse(string $expected, string $message = ''): void
+ {
+ $verboseMessage = $this->extractVerboseMessage($message);
+ $this->assertThat(null, new FileSent($this->_response), $verboseMessage);
+ $this->assertThat($expected, new FileSentAs($this->_response), $verboseMessage);
+ }
+
+ /**
+ * Inspect controller to extract possible causes of the failed assertion
+ *
+ * @param string $message Original message to use as a base
+ * @return string
+ */
+ protected function extractVerboseMessage(string $message): string
+ {
+ if ($this->_exception instanceof Exception) {
+ $message .= $this->extractExceptionMessage($this->_exception);
+ }
+ if ($this->_controller === null) {
+ return $message;
+ }
+ $error = $this->_controller->viewBuilder()->getVar('error');
+ if ($error instanceof Exception) {
+ $message .= $this->extractExceptionMessage($this->viewVariable('error'));
+ }
+
+ return $message;
+ }
+
+ /**
+ * Extract verbose message for existing exception
+ *
+ * @param \Exception $exception Exception to extract
+ * @return string
+ */
+ protected function extractExceptionMessage(Exception $exception): string
+ {
+ return PHP_EOL .
+ sprintf('Possibly related to %s: "%s" ', get_class($exception), $exception->getMessage()) .
+ PHP_EOL .
+ $exception->getTraceAsString();
+ }
+
+ /**
+ * @return \Cake\TestSuite\TestSession
+ */
+ protected function getSession(): TestSession
+ {
+ return new TestSession($_SESSION);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/LegacyCommandRunner.php b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyCommandRunner.php
new file mode 100644
index 000000000..ce5ae9d40
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyCommandRunner.php
@@ -0,0 +1,39 @@
+dispatch();
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/LegacyShellDispatcher.php b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyShellDispatcher.php
new file mode 100644
index 000000000..4f987cb0d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/LegacyShellDispatcher.php
@@ -0,0 +1,64 @@
+_io = $io;
+ parent::__construct($args, $bootstrap);
+ }
+
+ /**
+ * Injects mock and stub io components into the shell
+ *
+ * @param string $className Class name
+ * @param string $shortName Short name
+ * @return \Cake\Console\Shell
+ */
+ protected function _createShell(string $className, string $shortName): Shell
+ {
+ [$plugin] = pluginSplit($shortName);
+ /** @var \Cake\Console\Shell $instance */
+ $instance = new $className($this->_io);
+ if ($plugin) {
+ $instance->plugin = trim($plugin, '.');
+ }
+
+ return $instance;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php b/app/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php
new file mode 100644
index 000000000..7deff01f4
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php
@@ -0,0 +1,144 @@
+app = $app;
+ }
+
+ /**
+ * Resolve the provided URL into a string.
+ *
+ * @param array|string $url The URL array/string to resolve.
+ * @return string
+ */
+ public function resolveUrl($url): string
+ {
+ // If we need to resolve a Route URL but there are no routes, load routes.
+ if (is_array($url) && count(Router::getRouteCollection()->routes()) === 0) {
+ return $this->resolveRoute($url);
+ }
+
+ return Router::url($url);
+ }
+
+ /**
+ * Convert a URL array into a string URL via routing.
+ *
+ * @param array $url The url to resolve
+ * @return string
+ */
+ protected function resolveRoute(array $url): string
+ {
+ // Simulate application bootstrap and route loading.
+ // We need both to ensure plugins are loaded.
+ $this->app->bootstrap();
+ if ($this->app instanceof PluginApplicationInterface) {
+ $this->app->pluginBootstrap();
+ }
+ $builder = Router::createRouteBuilder('/');
+
+ if ($this->app instanceof RoutingApplicationInterface) {
+ $this->app->routes($builder);
+ }
+ if ($this->app instanceof PluginApplicationInterface) {
+ $this->app->pluginRoutes($builder);
+ }
+
+ $out = Router::url($url);
+ Router::resetRoutes();
+
+ return $out;
+ }
+
+ /**
+ * Create a PSR7 request from the request spec.
+ *
+ * @param array $spec The request spec.
+ * @return \Cake\Http\ServerRequest
+ */
+ protected function _createRequest(array $spec): ServerRequest
+ {
+ if (isset($spec['input'])) {
+ $spec['post'] = [];
+ $spec['environment']['CAKEPHP_INPUT'] = $spec['input'];
+ }
+ $environment = array_merge(
+ array_merge($_SERVER, ['REQUEST_URI' => $spec['url']]),
+ $spec['environment']
+ );
+ if (strpos($environment['PHP_SELF'], 'phpunit') !== false) {
+ $environment['PHP_SELF'] = '/';
+ }
+ $request = ServerRequestFactory::fromGlobals(
+ $environment,
+ $spec['query'],
+ $spec['post'],
+ $spec['cookies'],
+ $spec['files']
+ );
+ $request = $request
+ ->withAttribute('session', $spec['session'])
+ ->withAttribute('flash', new FlashMessage($spec['session']));
+
+ return $request;
+ }
+
+ /**
+ * Run a request and get the response.
+ *
+ * @param array $requestSpec The request spec to execute.
+ * @return \Psr\Http\Message\ResponseInterface The generated response.
+ * @throws \LogicException
+ */
+ public function execute(array $requestSpec): ResponseInterface
+ {
+ $server = new Server($this->app);
+
+ return $server->run($this->_createRequest($requestSpec));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php b/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php
new file mode 100644
index 000000000..bcac1cfa6
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/StringCompareTrait.php
@@ -0,0 +1,69 @@
+_compareBasePath . $path;
+ }
+
+ if ($this->_updateComparisons === null) {
+ $this->_updateComparisons = env('UPDATE_TEST_COMPARISON_FILES');
+ }
+
+ if ($this->_updateComparisons) {
+ file_put_contents($path, $result);
+ }
+
+ $expected = file_get_contents($path);
+ $this->assertTextEquals($expected, $result, 'Content does not match file ' . $path);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleInput.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleInput.php
new file mode 100644
index 000000000..2d03cb224
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleInput.php
@@ -0,0 +1,88 @@
+replies = $replies;
+ }
+
+ /**
+ * Read a reply
+ *
+ * @return string The value of the reply
+ */
+ public function read(): string
+ {
+ $this->currentIndex += 1;
+
+ if (!isset($this->replies[$this->currentIndex])) {
+ $total = count($this->replies);
+ $formatter = new NumberFormatter('en', NumberFormatter::ORDINAL);
+ $nth = $formatter->format($this->currentIndex + 1);
+
+ $replies = implode(', ', $this->replies);
+ $message = "There are no more input replies available. This is the {$nth} read operation, " .
+ "only {$total} replies were set.\nThe provided replies are: {$replies}";
+ throw new MissingConsoleInputException($message);
+ }
+
+ return $this->replies[$this->currentIndex];
+ }
+
+ /**
+ * Check if data is available on stdin
+ *
+ * @param int $timeout An optional time to wait for data
+ * @return bool True for data available, false otherwise
+ */
+ public function dataAvailable($timeout = 0): bool
+ {
+ return true;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleOutput.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleOutput.php
new file mode 100644
index 000000000..ac55fcca4
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/ConsoleOutput.php
@@ -0,0 +1,84 @@
+_out[] = $line;
+ }
+
+ $newlines--;
+ while ($newlines > 0) {
+ $this->_out[] = '';
+ $newlines--;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Get the buffered output.
+ *
+ * @return array
+ */
+ public function messages(): array
+ {
+ return $this->_out;
+ }
+
+ /**
+ * Get the output as a string
+ *
+ * @return string
+ */
+ public function output(): string
+ {
+ return implode("\n", $this->_out);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/MissingConsoleInputException.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/MissingConsoleInputException.php
new file mode 100644
index 000000000..3055b282a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/MissingConsoleInputException.php
@@ -0,0 +1,35 @@
+message .= "\nThe question asked was: " . $question;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/Stub/TestExceptionRenderer.php b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/TestExceptionRenderer.php
new file mode 100644
index 000000000..3122bdac7
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/Stub/TestExceptionRenderer.php
@@ -0,0 +1,44 @@
+markTestSkipped($message);
+ }
+
+ return $shouldSkip;
+ }
+
+ /**
+ * Helper method for tests that needs to use error_reporting()
+ *
+ * @param int $errorLevel value of error_reporting() that needs to use
+ * @param callable $callable callable function that will receive asserts
+ * @return void
+ */
+ public function withErrorReporting(int $errorLevel, callable $callable): void
+ {
+ $default = error_reporting();
+ error_reporting($errorLevel);
+ try {
+ $callable();
+ } finally {
+ error_reporting($default);
+ }
+ }
+
+ /**
+ * Helper method for check deprecation methods
+ *
+ * @param callable $callable callable function that will receive asserts
+ * @return void
+ */
+ public function deprecated(callable $callable): void
+ {
+ $errorLevel = error_reporting();
+ error_reporting(E_ALL ^ E_USER_DEPRECATED);
+ try {
+ $callable();
+ } finally {
+ error_reporting($errorLevel);
+ }
+ }
+
+ /**
+ * Setup the test case, backup the static object values so they can be restored.
+ * Specifically backs up the contents of Configure and paths in App if they have
+ * not already been backed up.
+ *
+ * @return void
+ */
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ if (!$this->_configure) {
+ $this->_configure = Configure::read();
+ }
+ if (class_exists(Router::class, false)) {
+ Router::reload();
+ }
+
+ EventManager::instance(new EventManager());
+ }
+
+ /**
+ * teardown any static object changes and restore them.
+ *
+ * @return void
+ */
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+ if ($this->_configure) {
+ Configure::clear();
+ Configure::write($this->_configure);
+ }
+ $this->getTableLocator()->clear();
+ $this->_configure = [];
+ $this->_tableLocator = null;
+ $this->fixtureManager = null;
+ }
+
+ /**
+ * Chooses which fixtures to load for a given test
+ *
+ * Each parameter is a model name that corresponds to a fixture, i.e. 'Posts', 'Authors', etc.
+ * Passing no parameters will cause all fixtures on the test case to load.
+ *
+ * @return void
+ * @see \Cake\TestSuite\TestCase::$autoFixtures
+ * @throws \RuntimeException when no fixture manager is available.
+ */
+ public function loadFixtures(): void
+ {
+ if ($this->autoFixtures) {
+ throw new RuntimeException('Cannot use `loadFixtures()` with `$autoFixtures` enabled.');
+ }
+ if ($this->fixtureManager === null) {
+ throw new RuntimeException('No fixture manager to load the test fixture');
+ }
+
+ $args = func_get_args();
+ foreach ($args as $class) {
+ $this->fixtureManager->loadSingle($class, null, $this->dropTables);
+ }
+
+ if (empty($args)) {
+ $autoFixtures = $this->autoFixtures;
+ $this->autoFixtures = true;
+ $this->fixtureManager->load($this);
+ $this->autoFixtures = $autoFixtures;
+ }
+ }
+
+ /**
+ * Load routes for the application.
+ *
+ * If no application class can be found an exception will be raised.
+ * Routes for plugins will *not* be loaded. Use `loadPlugins()` or use
+ * `Cake\TestSuite\IntegrationTestCaseTrait` to better simulate all routes
+ * and plugins being loaded.
+ *
+ * @param array|null $appArgs Constructor parameters for the application class.
+ * @return void
+ * @since 4.0.1
+ */
+ public function loadRoutes(?array $appArgs = null): void
+ {
+ $appArgs = $appArgs ?? [rtrim(CONFIG, DIRECTORY_SEPARATOR)];
+ /** @psalm-var class-string */
+ $className = Configure::read('App.namespace') . '\\Application';
+ try {
+ $reflect = new ReflectionClass($className);
+ /** @var \Cake\Routing\RoutingApplicationInterface $app */
+ $app = $reflect->newInstanceArgs($appArgs);
+ } catch (ReflectionException $e) {
+ throw new LogicException(sprintf('Cannot load "%s" to load routes from.', $className), 0, $e);
+ }
+ $builder = Router::createRouteBuilder('/');
+ $app->routes($builder);
+ }
+
+ /**
+ * Load plugins into a simulated application.
+ *
+ * Useful to test how plugins being loaded/not loaded interact with other
+ * elements in CakePHP or applications.
+ *
+ * @param array $plugins List of Plugins to load.
+ * @return \Cake\Http\BaseApplication
+ */
+ public function loadPlugins(array $plugins = []): BaseApplication
+ {
+ /** @var \Cake\Http\BaseApplication $app */
+ $app = $this->getMockForAbstractClass(
+ BaseApplication::class,
+ ['']
+ );
+
+ foreach ($plugins as $pluginName => $config) {
+ if (is_array($config)) {
+ $app->addPlugin($pluginName, $config);
+ } else {
+ $app->addPlugin($config);
+ }
+ }
+ $app->pluginBootstrap();
+ $builder = Router::createRouteBuilder('/');
+ $app->pluginRoutes($builder);
+
+ return $app;
+ }
+
+ /**
+ * Remove plugins from the global plugin collection.
+ *
+ * Useful in test case teardown methods.
+ *
+ * @param string[] $names A list of plugins you want to remove.
+ * @return void
+ */
+ public function removePlugins(array $names = []): void
+ {
+ $collection = Plugin::getCollection();
+ foreach ($names as $name) {
+ $collection->remove($name);
+ }
+ }
+
+ /**
+ * Clear all plugins from the global plugin collection.
+ *
+ * Useful in test case teardown methods.
+ *
+ * @return void
+ */
+ public function clearPlugins(): void
+ {
+ Plugin::getCollection()->clear();
+ }
+
+ /**
+ * Asserts that a global event was fired. You must track events in your event manager for this assertion to work
+ *
+ * @param string $name Event name
+ * @param \Cake\Event\EventManager|null $eventManager Event manager to check, defaults to global event manager
+ * @param string $message Assertion failure message
+ * @return void
+ */
+ public function assertEventFired(string $name, ?EventManager $eventManager = null, string $message = ''): void
+ {
+ if (!$eventManager) {
+ $eventManager = EventManager::instance();
+ }
+ $this->assertThat($name, new EventFired($eventManager), $message);
+ }
+
+ /**
+ * Asserts an event was fired with data
+ *
+ * If a third argument is passed, that value is used to compare with the value in $dataKey
+ *
+ * @param string $name Event name
+ * @param string $dataKey Data key
+ * @param mixed $dataValue Data value
+ * @param \Cake\Event\EventManager|null $eventManager Event manager to check, defaults to global event manager
+ * @param string $message Assertion failure message
+ * @return void
+ */
+ public function assertEventFiredWith(
+ string $name,
+ string $dataKey,
+ $dataValue,
+ ?EventManager $eventManager = null,
+ string $message = ''
+ ): void {
+ if (!$eventManager) {
+ $eventManager = EventManager::instance();
+ }
+ $this->assertThat($name, new EventFiredWith($eventManager, $dataKey, $dataValue), $message);
+ }
+
+ /**
+ * Assert text equality, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $expected The expected value.
+ * @param string $result The actual value.
+ * @param string $message The message to use for failure.
+ * @return void
+ */
+ public function assertTextNotEquals(string $expected, string $result, string $message = ''): void
+ {
+ $expected = str_replace(["\r\n", "\r"], "\n", $expected);
+ $result = str_replace(["\r\n", "\r"], "\n", $result);
+ $this->assertNotEquals($expected, $result, $message);
+ }
+
+ /**
+ * Assert text equality, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $expected The expected value.
+ * @param string $result The actual value.
+ * @param string $message The message to use for failure.
+ * @return void
+ */
+ public function assertTextEquals(string $expected, string $result, string $message = ''): void
+ {
+ $expected = str_replace(["\r\n", "\r"], "\n", $expected);
+ $result = str_replace(["\r\n", "\r"], "\n", $result);
+ $this->assertEquals($expected, $result, $message);
+ }
+
+ /**
+ * Asserts that a string starts with a given prefix, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $prefix The prefix to check for.
+ * @param string $string The string to search in.
+ * @param string $message The message to use for failure.
+ * @return void
+ */
+ public function assertTextStartsWith(string $prefix, string $string, string $message = ''): void
+ {
+ $prefix = str_replace(["\r\n", "\r"], "\n", $prefix);
+ $string = str_replace(["\r\n", "\r"], "\n", $string);
+ $this->assertStringStartsWith($prefix, $string, $message);
+ }
+
+ /**
+ * Asserts that a string starts not with a given prefix, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $prefix The prefix to not find.
+ * @param string $string The string to search.
+ * @param string $message The message to use for failure.
+ * @return void
+ */
+ public function assertTextStartsNotWith(string $prefix, string $string, string $message = ''): void
+ {
+ $prefix = str_replace(["\r\n", "\r"], "\n", $prefix);
+ $string = str_replace(["\r\n", "\r"], "\n", $string);
+ $this->assertStringStartsNotWith($prefix, $string, $message);
+ }
+
+ /**
+ * Asserts that a string ends with a given prefix, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $suffix The suffix to find.
+ * @param string $string The string to search.
+ * @param string $message The message to use for failure.
+ * @return void
+ */
+ public function assertTextEndsWith(string $suffix, string $string, string $message = ''): void
+ {
+ $suffix = str_replace(["\r\n", "\r"], "\n", $suffix);
+ $string = str_replace(["\r\n", "\r"], "\n", $string);
+ $this->assertStringEndsWith($suffix, $string, $message);
+ }
+
+ /**
+ * Asserts that a string ends not with a given prefix, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $suffix The suffix to not find.
+ * @param string $string The string to search.
+ * @param string $message The message to use for failure.
+ * @return void
+ */
+ public function assertTextEndsNotWith(string $suffix, string $string, string $message = ''): void
+ {
+ $suffix = str_replace(["\r\n", "\r"], "\n", $suffix);
+ $string = str_replace(["\r\n", "\r"], "\n", $string);
+ $this->assertStringEndsNotWith($suffix, $string, $message);
+ }
+
+ /**
+ * Assert that a string contains another string, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $needle The string to search for.
+ * @param string $haystack The string to search through.
+ * @param string $message The message to display on failure.
+ * @param bool $ignoreCase Whether or not the search should be case-sensitive.
+ * @return void
+ */
+ public function assertTextContains(
+ string $needle,
+ string $haystack,
+ string $message = '',
+ bool $ignoreCase = false
+ ): void {
+ $needle = str_replace(["\r\n", "\r"], "\n", $needle);
+ $haystack = str_replace(["\r\n", "\r"], "\n", $haystack);
+
+ if ($ignoreCase) {
+ $this->assertStringContainsStringIgnoringCase($needle, $haystack, $message);
+ } else {
+ $this->assertStringContainsString($needle, $haystack, $message);
+ }
+ }
+
+ /**
+ * Assert that a text doesn't contain another text, ignoring differences in newlines.
+ * Helpful for doing cross platform tests of blocks of text.
+ *
+ * @param string $needle The string to search for.
+ * @param string $haystack The string to search through.
+ * @param string $message The message to display on failure.
+ * @param bool $ignoreCase Whether or not the search should be case-sensitive.
+ * @return void
+ */
+ public function assertTextNotContains(
+ string $needle,
+ string $haystack,
+ string $message = '',
+ bool $ignoreCase = false
+ ): void {
+ $needle = str_replace(["\r\n", "\r"], "\n", $needle);
+ $haystack = str_replace(["\r\n", "\r"], "\n", $haystack);
+
+ if ($ignoreCase) {
+ $this->assertStringNotContainsStringIgnoringCase($needle, $haystack, $message);
+ } else {
+ $this->assertStringNotContainsString($needle, $haystack, $message);
+ }
+ }
+
+ /**
+ * Assert that a string matches SQL with db-specific characters like quotes removed.
+ *
+ * @param string $expected The expected sql
+ * @param string $actual The sql to compare
+ * @param string $message The message to display on failure
+ * @return void
+ */
+ public function assertEqualsSql(
+ string $expected,
+ string $actual,
+ string $message = ''
+ ): void {
+ $this->assertEquals($expected, preg_replace('/[`"\[\]]/', '', $actual), $message);
+ }
+
+ /**
+ * Assertion for comparing a regex pattern against a query having its identifiers
+ * quoted. It accepts queries quoted with the characters `<` and `>`. If the third
+ * parameter is set to true, it will alter the pattern to both accept quoted and
+ * unquoted queries
+ *
+ * @param string $pattern The expected sql pattern
+ * @param string $actual The sql to compare
+ * @param bool $optional Whether quote characters (marked with <>) are optional
+ * @return void
+ */
+ public function assertRegExpSql(string $pattern, string $actual, bool $optional = false): void
+ {
+ $optional = $optional ? '?' : '';
+ $pattern = str_replace('<', '[`"\[]' . $optional, $pattern);
+ $pattern = str_replace('>', '[`"\]]' . $optional, $pattern);
+ $this->assertMatchesRegularExpression('#' . $pattern . '#', $actual);
+ }
+
+ /**
+ * Asserts HTML tags.
+ *
+ * Takes an array $expected and generates a regex from it to match the provided $string.
+ * Samples for $expected:
+ *
+ * Checks for an input tag with a name attribute (contains any non-empty value) and an id
+ * attribute that contains 'my-input':
+ *
+ * ```
+ * ['input' => ['name', 'id' => 'my-input']]
+ * ```
+ *
+ * Checks for two p elements with some text in them:
+ *
+ * ```
+ * [
+ * ['p' => true],
+ * 'textA',
+ * '/p',
+ * ['p' => true],
+ * 'textB',
+ * '/p'
+ * ]
+ * ```
+ *
+ * You can also specify a pattern expression as part of the attribute values, or the tag
+ * being defined, if you prepend the value with preg: and enclose it with slashes, like so:
+ *
+ * ```
+ * [
+ * ['input' => ['name', 'id' => 'preg:/FieldName\d+/']],
+ * 'preg:/My\s+field/'
+ * ]
+ * ```
+ *
+ * Important: This function is very forgiving about whitespace and also accepts any
+ * permutation of attribute order. It will also allow whitespace between specified tags.
+ *
+ * @param array $expected An array, see above
+ * @param string $string An HTML/XHTML/XML string
+ * @param bool $fullDebug Whether or not more verbose output should be used.
+ * @return bool
+ */
+ public function assertHtml(array $expected, string $string, bool $fullDebug = false): bool
+ {
+ $regex = [];
+ $normalized = [];
+ foreach ($expected as $key => $val) {
+ if (!is_numeric($key)) {
+ $normalized[] = [$key => $val];
+ } else {
+ $normalized[] = $val;
+ }
+ }
+ $i = 0;
+ foreach ($normalized as $tags) {
+ if (!is_array($tags)) {
+ $tags = (string)$tags;
+ }
+ $i++;
+ if (is_string($tags) && $tags[0] === '<') {
+ /** @psalm-suppress InvalidArrayOffset */
+ $tags = [substr($tags, 1) => []];
+ } elseif (is_string($tags)) {
+ $tagsTrimmed = preg_replace('/\s+/m', '', $tags);
+
+ if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') {
+ $prefix = ['', ''];
+
+ if ($match[0] === '*/') {
+ $prefix = ['Anything, ', '.*?'];
+ }
+ $regex[] = [
+ sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))),
+ sprintf('%s\s*<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr($tags, strlen($match[0]))),
+ $i,
+ ];
+ continue;
+ }
+ if (!empty($tags) && preg_match('/^preg\:\/(.+)\/$/i', $tags, $matches)) {
+ $tags = $matches[1];
+ $type = 'Regex matches';
+ } else {
+ $tags = '\s*' . preg_quote($tags, '/');
+ $type = 'Text equals';
+ }
+ $regex[] = [
+ sprintf('%s "%s"', $type, $tags),
+ $tags,
+ $i,
+ ];
+ continue;
+ }
+ foreach ($tags as $tag => $attributes) {
+ /** @psalm-suppress PossiblyFalseArgument */
+ $regex[] = [
+ sprintf('Open %s tag', $tag),
+ sprintf('[\s]*<%s', preg_quote($tag, '/')),
+ $i,
+ ];
+ if ($attributes === true) {
+ $attributes = [];
+ }
+ $attrs = [];
+ $explanations = [];
+ $i = 1;
+ foreach ($attributes as $attr => $val) {
+ if (is_numeric($attr) && preg_match('/^preg\:\/(.+)\/$/i', (string)$val, $matches)) {
+ $attrs[] = $matches[1];
+ $explanations[] = sprintf('Regex "%s" matches', $matches[1]);
+ continue;
+ }
+ $val = (string)$val;
+
+ $quotes = '["\']';
+ if (is_numeric($attr)) {
+ $attr = $val;
+ $val = '.+?';
+ $explanations[] = sprintf('Attribute "%s" present', $attr);
+ } elseif (!empty($val) && preg_match('/^preg\:\/(.+)\/$/i', $val, $matches)) {
+ $val = str_replace(
+ ['.*', '.+'],
+ ['.*?', '.+?'],
+ $matches[1]
+ );
+ $quotes = $val !== $matches[1] ? '["\']' : '["\']?';
+
+ $explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
+ } else {
+ $explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val);
+ $val = preg_quote($val, '/');
+ }
+ $attrs[] = '[\s]+' . preg_quote($attr, '/') . '=' . $quotes . $val . $quotes;
+ $i++;
+ }
+ if ($attrs) {
+ $regex[] = [
+ 'explains' => $explanations,
+ 'attrs' => $attrs,
+ ];
+ }
+ /** @psalm-suppress PossiblyFalseArgument */
+ $regex[] = [
+ sprintf('End %s tag', $tag),
+ '[\s]*\/?[\s]*>[\n\r]*',
+ $i,
+ ];
+ }
+ }
+ foreach ($regex as $i => $assertion) {
+ $matches = false;
+ if (isset($assertion['attrs'])) {
+ $string = $this->_assertAttributes($assertion, $string, $fullDebug, $regex);
+ if ($fullDebug === true && $string === false) {
+ debug($string, true);
+ debug($regex, true);
+ }
+ continue;
+ }
+
+ // If 'attrs' is not present then the array is just a regular int-offset one
+ /** @psalm-suppress PossiblyUndefinedArrayOffset */
+ [$description, $expressions, $itemNum] = $assertion;
+ $expression = '';
+ foreach ((array)$expressions as $expression) {
+ $expression = sprintf('/^%s/s', $expression);
+ if (preg_match($expression, $string, $match)) {
+ $matches = true;
+ $string = substr($string, strlen($match[0]));
+ break;
+ }
+ }
+ if (!$matches) {
+ if ($fullDebug === true) {
+ debug($string);
+ debug($regex);
+ }
+ $this->assertMatchesRegularExpression(
+ $expression,
+ $string,
+ sprintf('Item #%d / regex #%d failed: %s', $itemNum, $i, $description)
+ );
+
+ return false;
+ }
+ }
+
+ $this->assertTrue(true, '%s');
+
+ return true;
+ }
+
+ /**
+ * Check the attributes as part of an assertTags() check.
+ *
+ * @param array $assertions Assertions to run.
+ * @param string $string The HTML string to check.
+ * @param bool $fullDebug Whether or not more verbose output should be used.
+ * @param array|string $regex Full regexp from `assertHtml`
+ * @return string|false
+ */
+ protected function _assertAttributes(array $assertions, string $string, bool $fullDebug = false, $regex = '')
+ {
+ $asserts = $assertions['attrs'];
+ $explains = $assertions['explains'];
+ do {
+ $matches = false;
+ $j = null;
+ foreach ($asserts as $j => $assert) {
+ if (preg_match(sprintf('/^%s/s', $assert), $string, $match)) {
+ $matches = true;
+ $string = substr($string, strlen($match[0]));
+ array_splice($asserts, $j, 1);
+ array_splice($explains, $j, 1);
+ break;
+ }
+ }
+ if ($matches === false) {
+ if ($fullDebug === true) {
+ debug($string);
+ debug($regex);
+ }
+ $this->assertTrue(false, 'Attribute did not match. Was expecting ' . $explains[$j]);
+ }
+ $len = count($asserts);
+ } while ($len > 0);
+
+ return $string;
+ }
+
+ /**
+ * Normalize a path for comparison.
+ *
+ * @param string $path Path separated by "/" slash.
+ * @return string Normalized path separated by DIRECTORY_SEPARATOR.
+ */
+ protected function _normalizePath(string $path): string
+ {
+ return str_replace('/', DIRECTORY_SEPARATOR, $path);
+ }
+
+// phpcs:disable
+
+ /**
+ * Compatibility function to test if a value is between an acceptable range.
+ *
+ * @param float $expected
+ * @param float $result
+ * @param float $margin the rage of acceptation
+ * @param string $message the text to display if the assertion is not correct
+ * @return void
+ */
+ protected static function assertWithinRange($expected, $result, $margin, $message = '')
+ {
+ $upper = $result + $margin;
+ $lower = $result - $margin;
+ static::assertTrue(($expected <= $upper) && ($expected >= $lower), $message);
+ }
+
+ /**
+ * Compatibility function to test if a value is not between an acceptable range.
+ *
+ * @param float $expected
+ * @param float $result
+ * @param float $margin the rage of acceptation
+ * @param string $message the text to display if the assertion is not correct
+ * @return void
+ */
+ protected static function assertNotWithinRange($expected, $result, $margin, $message = '')
+ {
+ $upper = $result + $margin;
+ $lower = $result - $margin;
+ static::assertTrue(($expected > $upper) || ($expected < $lower), $message);
+ }
+
+ /**
+ * Compatibility function to test paths.
+ *
+ * @param string $expected
+ * @param string $result
+ * @param string $message the text to display if the assertion is not correct
+ * @return void
+ */
+ protected static function assertPathEquals($expected, $result, $message = '')
+ {
+ $expected = str_replace(DIRECTORY_SEPARATOR, '/', $expected);
+ $result = str_replace(DIRECTORY_SEPARATOR, '/', $result);
+ static::assertEquals($expected, $result, $message);
+ }
+
+ /**
+ * Compatibility function for skipping.
+ *
+ * @param bool $condition Condition to trigger skipping
+ * @param string $message Message for skip
+ * @return bool
+ */
+ protected function skipUnless($condition, $message = '')
+ {
+ if (!$condition) {
+ $this->markTestSkipped($message);
+ }
+
+ return $condition;
+ }
+
+// phpcs:enable
+
+ /**
+ * Mock a model, maintain fixtures and table association
+ *
+ * @param string $alias The model to get a mock for.
+ * @param string[] $methods The list of methods to mock
+ * @param array $options The config data for the mock's constructor.
+ * @throws \Cake\ORM\Exception\MissingTableClassException
+ * @return \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject
+ */
+ public function getMockForModel(string $alias, array $methods = [], array $options = [])
+ {
+ $className = $this->_getTableClassName($alias, $options);
+ $connectionName = $className::defaultConnectionName();
+ $connection = ConnectionManager::get($connectionName);
+
+ $locator = $this->getTableLocator();
+
+ [, $baseClass] = pluginSplit($alias);
+ $options += ['alias' => $baseClass, 'connection' => $connection];
+ $options += $locator->getConfig($alias);
+ $reflection = new ReflectionClass($className);
+ $classMethods = array_map(function ($method) {
+ return $method->name;
+ }, $reflection->getMethods());
+
+ $existingMethods = array_intersect($classMethods, $methods);
+ $nonExistingMethods = array_diff($methods, $existingMethods);
+
+ $builder = $this->getMockBuilder($className)
+ ->setConstructorArgs([$options]);
+
+ if ($existingMethods || !$nonExistingMethods) {
+ $builder->onlyMethods($existingMethods);
+ }
+
+ if ($nonExistingMethods) {
+ $builder->addMethods($nonExistingMethods);
+ }
+
+ /** @var \Cake\ORM\Table $mock */
+ $mock = $builder->getMock();
+
+ if (empty($options['entityClass']) && $mock->getEntityClass() === Entity::class) {
+ $parts = explode('\\', $className);
+ $entityAlias = Inflector::classify(Inflector::underscore(substr(array_pop($parts), 0, -5)));
+ $entityClass = implode('\\', array_slice($parts, 0, -1)) . '\\Entity\\' . $entityAlias;
+ if (class_exists($entityClass)) {
+ $mock->setEntityClass($entityClass);
+ }
+ }
+
+ if (stripos($mock->getTable(), 'mock') === 0) {
+ $mock->setTable(Inflector::tableize($baseClass));
+ }
+
+ $locator->set($baseClass, $mock);
+ $locator->set($alias, $mock);
+
+ return $mock;
+ }
+
+ /**
+ * Gets the class name for the table.
+ *
+ * @param string $alias The model to get a mock for.
+ * @param array $options The config data for the mock's constructor.
+ * @return string
+ * @throws \Cake\ORM\Exception\MissingTableClassException
+ * @psalm-return class-string<\Cake\ORM\Table>
+ */
+ protected function _getTableClassName(string $alias, array $options): string
+ {
+ if (empty($options['className'])) {
+ $class = Inflector::camelize($alias);
+ /** @psalm-var class-string<\Cake\ORM\Table>|null */
+ $className = App::className($class, 'Model/Table', 'Table');
+ if (!$className) {
+ throw new MissingTableClassException([$alias]);
+ }
+ $options['className'] = $className;
+ }
+
+ return $options['className'];
+ }
+
+ /**
+ * Set the app namespace
+ *
+ * @param string $appNamespace The app namespace, defaults to "TestApp".
+ * @return string|null The previous app namespace or null if not set.
+ */
+ public static function setAppNamespace(string $appNamespace = 'TestApp'): ?string
+ {
+ $previous = Configure::read('App.namespace');
+ Configure::write('App.namespace', $appNamespace);
+
+ return $previous;
+ }
+
+ /**
+ * Adds a fixture to this test case.
+ *
+ * Examples:
+ * - core.Tags
+ * - app.MyRecords
+ * - plugin.MyPluginName.MyModelName
+ *
+ * Use this method inside your test cases' {@link getFixtures()} method
+ * to build up the fixture list.
+ *
+ * @param string $fixture Fixture
+ * @return $this
+ */
+ protected function addFixture(string $fixture)
+ {
+ $this->fixtures[] = $fixture;
+
+ return $this;
+ }
+
+ /**
+ * Gets fixtures.
+ *
+ * @return string[]
+ */
+ public function getFixtures(): array
+ {
+ return $this->fixtures;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/TestEmailTransport.php b/app/vendor/cakephp/cakephp/src/TestSuite/TestEmailTransport.php
new file mode 100644
index 000000000..1843c1a02
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/TestEmailTransport.php
@@ -0,0 +1,87 @@
+session = $session;
+ }
+
+ /**
+ * Returns true if given variable name is set in session.
+ *
+ * @param string|null $name Variable name to check for
+ * @return bool True if variable is there
+ */
+ public function check(?string $name = null): bool
+ {
+ if ($this->session === null) {
+ return false;
+ }
+
+ if ($name === null) {
+ return (bool)$this->session;
+ }
+
+ return Hash::get($this->session, $name) !== null;
+ }
+
+ /**
+ * Returns given session variable, or all of them, if no parameters given.
+ *
+ * @param string|null $name The name of the session variable (or a path as sent to Hash.extract)
+ * @return mixed The value of the session variable, null if session not available,
+ * session not started, or provided name not found in the session.
+ */
+ public function read(?string $name = null)
+ {
+ if ($this->session === null) {
+ return null;
+ }
+
+ if ($name === null) {
+ return $this->session ?: [];
+ }
+
+ return Hash::get($this->session, $name);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/TestSuite/TestSuite.php b/app/vendor/cakephp/cakephp/src/TestSuite/TestSuite.php
new file mode 100644
index 000000000..e5a18c215
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/TestSuite/TestSuite.php
@@ -0,0 +1,66 @@
+find($directory, '/\.php$/');
+ foreach ($files as $file => $fileInfo) {
+ $this->addTestFile($file);
+ }
+ }
+
+ /**
+ * Recursively adds all the files in a directory to the test suite.
+ *
+ * @param string $directory The directory subtree to add tests from.
+ * @return void
+ */
+ public function addTestDirectoryRecursive(string $directory = '.'): void
+ {
+ $fs = new Filesystem();
+ $files = $fs->findRecursive($directory, function (SplFileInfo $current) {
+ $file = $current->getFilename();
+ if ($file[0] === '.' || !preg_match('/\.php$/', $file)) {
+ return false;
+ }
+
+ return true;
+ });
+ foreach ($files as $file => $fileInfo) {
+ $this->addTestFile($file);
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/CookieCryptTrait.php b/app/vendor/cakephp/cakephp/src/Utility/CookieCryptTrait.php
new file mode 100644
index 000000000..afe10e028
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/CookieCryptTrait.php
@@ -0,0 +1,192 @@
+_implode($value);
+ }
+ if ($encrypt === false) {
+ return $value;
+ }
+ $this->_checkCipher($encrypt);
+ $prefix = 'Q2FrZQ==.';
+ $cipher = '';
+ if ($key === null) {
+ $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 RuntimeException($msg);
+ }
+ }
+
+ /**
+ * Decrypts $value using public $type method in Security class
+ *
+ * @param string[]|string $values Values to decrypt
+ * @param string|false $mode Encryption mode
+ * @param string|null $key Used as the security salt if specified.
+ * @return string|array Decrypted values
+ */
+ protected function _decrypt($values, $mode, ?string $key = null)
+ {
+ 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 string|array Decoded values.
+ */
+ protected function _decode(string $value, $encrypt, ?string $key)
+ {
+ 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 '';
+ }
+
+ if ($key === null) {
+ $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);
+ }
+
+ /**
+ * 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 string|array Map of key and values
+ */
+ protected function _explode(string $string)
+ {
+ $first = substr($string, 0, 1);
+ if ($first === '{' || $first === '[') {
+ $ret = json_decode($string, true);
+
+ return $ret ?? $string;
+ }
+ $array = [];
+ foreach (explode(',', $string) as $pair) {
+ $key = explode('|', $pair);
+ if (!isset($key[1])) {
+ return $key[0];
+ }
+ $array[$key[0]] = $key[1];
+ }
+
+ return $array;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/Crypto/OpenSsl.php b/app/vendor/cakephp/cakephp/src/Utility/Crypto/OpenSsl.php
new file mode 100644
index 000000000..044bf3f68
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/Crypto/OpenSsl.php
@@ -0,0 +1,79 @@
+`, `<`, `>=`, `<=` Value comparison.
+ * - `=/.../` Regular expression pattern match.
+ *
+ * Given a set of User array data, from a `$usersTable->find('all')` call:
+ *
+ * - `1.User.name` Get the name of the user at index 1.
+ * - `{n}.User.name` Get the name of every user in the set of users.
+ * - `{n}.User[id].name` Get the name of every user with an id key.
+ * - `{n}.User[id>=2].name` Get the name of every user with an id key greater than or equal to 2.
+ * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`.
+ * - `{n}.User[id=1].name` Get the Users name with id matching `1`.
+ *
+ * @param array|\ArrayAccess $data The data to extract from.
+ * @param string $path The path to extract.
+ * @return array|\ArrayAccess An array of the extracted values. Returns an empty array
+ * if there are no matches.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::extract
+ */
+ public static function extract($data, string $path)
+ {
+ if (!(is_array($data) || $data instanceof ArrayAccess)) {
+ throw new InvalidArgumentException(
+ 'Invalid data type, must be an array or \ArrayAccess instance.'
+ );
+ }
+
+ if (empty($path)) {
+ return $data;
+ }
+
+ // Simple paths.
+ if (!preg_match('/[{\[]/', $path)) {
+ $data = static::get($data, $path);
+ if ($data !== null && !(is_array($data) || $data instanceof ArrayAccess)) {
+ return [$data];
+ }
+
+ return $data !== null ? (array)$data : [];
+ }
+
+ if (strpos($path, '[') === false) {
+ $tokens = explode('.', $path);
+ } else {
+ $tokens = Text::tokenize($path, '.', '[', ']');
+ }
+
+ $_key = '__set_item__';
+
+ $context = [$_key => [$data]];
+
+ foreach ($tokens as $token) {
+ $next = [];
+
+ [$token, $conditions] = self::_splitConditions($token);
+
+ foreach ($context[$_key] as $item) {
+ if (is_object($item) && method_exists($item, 'toArray')) {
+ /** @var \Cake\Datasource\EntityInterface $item */
+ $item = $item->toArray();
+ }
+ foreach ((array)$item as $k => $v) {
+ if (static::_matchToken($k, $token)) {
+ $next[] = $v;
+ }
+ }
+ }
+
+ // Filter for attributes.
+ if ($conditions) {
+ $filter = [];
+ foreach ($next as $item) {
+ if (
+ (
+ is_array($item) ||
+ $item instanceof ArrayAccess
+ ) &&
+ static::_matches($item, $conditions)
+ ) {
+ $filter[] = $item;
+ }
+ }
+ $next = $filter;
+ }
+ $context = [$_key => $next];
+ }
+
+ return $context[$_key];
+ }
+
+ /**
+ * Split token conditions
+ *
+ * @param string $token the token being splitted.
+ * @return array [token, conditions] with token splitted
+ */
+ protected static function _splitConditions(string $token): array
+ {
+ $conditions = false;
+ $position = strpos($token, '[');
+ if ($position !== false) {
+ $conditions = substr($token, $position);
+ $token = substr($token, 0, $position);
+ }
+
+ return [$token, $conditions];
+ }
+
+ /**
+ * Check a key against a token.
+ *
+ * @param mixed $key The key in the array being searched.
+ * @param string $token The token being matched.
+ * @return bool
+ */
+ protected static function _matchToken($key, string $token): bool
+ {
+ switch ($token) {
+ case '{n}':
+ return is_numeric($key);
+ case '{s}':
+ return is_string($key);
+ case '{*}':
+ return true;
+ default:
+ return is_numeric($token) ? ($key == $token) : $key === $token;
+ }
+ }
+
+ /**
+ * Checks whether or not $data matches the attribute patterns
+ *
+ * @param array|\ArrayAccess $data Array of data to match.
+ * @param string $selector The patterns to match.
+ * @return bool Fitness of expression.
+ */
+ protected static function _matches($data, string $selector): bool
+ {
+ preg_match_all(
+ '/(\[ (?P[^=>[><]) \s* (?P(?:\/.*?\/ | [^\]]+)) )? \])/x',
+ $selector,
+ $conditions,
+ PREG_SET_ORDER
+ );
+
+ foreach ($conditions as $cond) {
+ $attr = $cond['attr'];
+ $op = $cond['op'] ?? null;
+ $val = $cond['val'] ?? null;
+
+ // Presence test.
+ if (empty($op) && empty($val) && !isset($data[$attr])) {
+ return false;
+ }
+
+ if (is_array($data)) {
+ $attrPresent = array_key_exists($attr, $data);
+ } else {
+ $attrPresent = $data->offsetExists($attr);
+ }
+ // Empty attribute = fail.
+ if (!$attrPresent) {
+ return false;
+ }
+
+ $prop = '';
+ if (isset($data[$attr])) {
+ $prop = $data[$attr];
+ }
+ $isBool = is_bool($prop);
+ if ($isBool && is_numeric($val)) {
+ $prop = $prop ? '1' : '0';
+ } elseif ($isBool) {
+ $prop = $prop ? 'true' : 'false';
+ } elseif (is_numeric($prop)) {
+ $prop = (string)$prop;
+ }
+
+ // Pattern matches and other operators.
+ if ($op === '=' && $val && $val[0] === '/') {
+ if (!preg_match($val, $prop)) {
+ return false;
+ }
+ // phpcs:disable
+ } elseif (
+ ($op === '=' && $prop != $val) ||
+ ($op === '!=' && $prop == $val) ||
+ ($op === '>' && $prop <= $val) ||
+ ($op === '<' && $prop >= $val) ||
+ ($op === '>=' && $prop < $val) ||
+ ($op === '<=' && $prop > $val)
+ // phpcs:enable
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Insert $values into an array with the given $path. You can use
+ * `{n}` and `{s}` elements to insert $data multiple times.
+ *
+ * @param array $data The data to insert into.
+ * @param string $path The path to insert at.
+ * @param mixed $values The values to insert.
+ * @return array The data with $values inserted.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::insert
+ */
+ public static function insert(array $data, string $path, $values = null): array
+ {
+ $noTokens = strpos($path, '[') === false;
+ if ($noTokens && strpos($path, '.') === false) {
+ $data[$path] = $values;
+
+ return $data;
+ }
+
+ if ($noTokens) {
+ $tokens = explode('.', $path);
+ } else {
+ $tokens = Text::tokenize($path, '.', '[', ']');
+ }
+
+ if ($noTokens && strpos($path, '{') === false) {
+ return static::_simpleOp('insert', $data, $tokens, $values);
+ }
+
+ $token = array_shift($tokens);
+ $nextPath = implode('.', $tokens);
+
+ [$token, $conditions] = static::_splitConditions($token);
+
+ foreach ($data as $k => $v) {
+ if (static::_matchToken($k, $token)) {
+ if (!$conditions || static::_matches($v, $conditions)) {
+ $data[$k] = $nextPath
+ ? static::insert($v, $nextPath, $values)
+ : array_merge($v, (array)$values);
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Perform a simple insert/remove operation.
+ *
+ * @param string $op The operation to do.
+ * @param array $data The data to operate on.
+ * @param string[] $path The path to work on.
+ * @param mixed $values The values to insert when doing inserts.
+ * @return array data.
+ */
+ protected static function _simpleOp(string $op, array $data, array $path, $values = null): array
+ {
+ $_list = &$data;
+
+ $count = count($path);
+ $last = $count - 1;
+ foreach ($path as $i => $key) {
+ if ($op === 'insert') {
+ if ($i === $last) {
+ $_list[$key] = $values;
+
+ return $data;
+ }
+ if (!isset($_list[$key])) {
+ $_list[$key] = [];
+ }
+ $_list = &$_list[$key];
+ if (!is_array($_list)) {
+ $_list = [];
+ }
+ } elseif ($op === 'remove') {
+ if ($i === $last) {
+ if (is_array($_list)) {
+ unset($_list[$key]);
+ }
+
+ return $data;
+ }
+ if (!isset($_list[$key])) {
+ return $data;
+ }
+ $_list = &$_list[$key];
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Remove data matching $path from the $data array.
+ * You can use `{n}` and `{s}` to remove multiple elements
+ * from $data.
+ *
+ * @param array $data The data to operate on
+ * @param string $path A path expression to use to remove.
+ * @return array The modified array.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::remove
+ */
+ public static function remove(array $data, string $path): array
+ {
+ $noTokens = strpos($path, '[') === false;
+ $noExpansion = strpos($path, '{') === false;
+
+ if ($noExpansion && $noTokens && strpos($path, '.') === false) {
+ unset($data[$path]);
+
+ return $data;
+ }
+
+ $tokens = $noTokens ? explode('.', $path) : Text::tokenize($path, '.', '[', ']');
+
+ if ($noExpansion && $noTokens) {
+ return static::_simpleOp('remove', $data, $tokens);
+ }
+
+ $token = array_shift($tokens);
+ $nextPath = implode('.', $tokens);
+
+ [$token, $conditions] = self::_splitConditions($token);
+
+ foreach ($data as $k => $v) {
+ $match = static::_matchToken($k, $token);
+ if ($match && is_array($v)) {
+ if ($conditions) {
+ if (static::_matches($v, $conditions)) {
+ if ($nextPath !== '') {
+ $data[$k] = static::remove($v, $nextPath);
+ } else {
+ unset($data[$k]);
+ }
+ }
+ } else {
+ $data[$k] = static::remove($v, $nextPath);
+ }
+ if (empty($data[$k])) {
+ unset($data[$k]);
+ }
+ } elseif ($match && $nextPath === '') {
+ unset($data[$k]);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Creates an associative array using `$keyPath` as the path to build its keys, and optionally
+ * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized
+ * to null (useful for Hash::merge). You can optionally group the values by what is obtained when
+ * following the path specified in `$groupPath`.
+ *
+ * @param array $data Array from where to extract keys and values
+ * @param string|string[]|null $keyPath A dot-separated string.
+ * @param string|string[]|null $valuePath A dot-separated string.
+ * @param string|null $groupPath A dot-separated string.
+ * @return array Combined array
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::combine
+ * @throws \RuntimeException When keys and values count is unequal.
+ */
+ public static function combine(array $data, $keyPath, $valuePath = null, ?string $groupPath = null): array
+ {
+ if (empty($data)) {
+ return [];
+ }
+
+ if (is_array($keyPath)) {
+ $format = array_shift($keyPath);
+ /** @var array $keys */
+ $keys = static::format($data, $keyPath, $format);
+ } elseif ($keyPath === null) {
+ $keys = $keyPath;
+ } else {
+ /** @var array $keys */
+ $keys = static::extract($data, $keyPath);
+ }
+ if ($keyPath !== null && empty($keys)) {
+ return [];
+ }
+
+ $vals = null;
+ if (!empty($valuePath) && is_array($valuePath)) {
+ $format = array_shift($valuePath);
+ /** @var array $vals */
+ $vals = static::format($data, $valuePath, $format);
+ } elseif (!empty($valuePath)) {
+ /** @var array $vals */
+ $vals = static::extract($data, $valuePath);
+ }
+ if (empty($vals)) {
+ $vals = array_fill(0, $keys === null ? count($data) : count($keys), null);
+ }
+
+ if (is_array($keys) && count($keys) !== count($vals)) {
+ throw new RuntimeException(
+ 'Hash::combine() needs an equal number of keys + values.'
+ );
+ }
+
+ if ($groupPath !== null) {
+ $group = static::extract($data, $groupPath);
+ if (!empty($group)) {
+ $c = is_array($keys) ? count($keys) : count($vals);
+ $out = [];
+ for ($i = 0; $i < $c; $i++) {
+ if (!isset($group[$i])) {
+ $group[$i] = 0;
+ }
+ if (!isset($out[$group[$i]])) {
+ $out[$group[$i]] = [];
+ }
+ if ($keys === null) {
+ $out[$group[$i]][] = $vals[$i];
+ } else {
+ $out[$group[$i]][$keys[$i]] = $vals[$i];
+ }
+ }
+
+ return $out;
+ }
+ }
+ if (empty($vals)) {
+ return [];
+ }
+
+ return array_combine($keys ?? range(0, count($vals) - 1), $vals);
+ }
+
+ /**
+ * Returns a formatted series of values extracted from `$data`, using
+ * `$format` as the format and `$paths` as the values to extract.
+ *
+ * Usage:
+ *
+ * ```
+ * $result = Hash::format($users, ['{n}.User.id', '{n}.User.name'], '%s : %s');
+ * ```
+ *
+ * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do.
+ *
+ * @param array $data Source array from which to extract the data
+ * @param string[] $paths An array containing one or more Hash::extract()-style key paths
+ * @param string $format Format string into which values will be inserted, see sprintf()
+ * @return string[]|null An array of strings extracted from `$path` and formatted with `$format`
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::format
+ * @see sprintf()
+ * @see \Cake\Utility\Hash::extract()
+ */
+ public static function format(array $data, array $paths, string $format): ?array
+ {
+ $extracted = [];
+ $count = count($paths);
+
+ if (!$count) {
+ return null;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $extracted[] = static::extract($data, $paths[$i]);
+ }
+ $out = [];
+ /** @var array $data */
+ $data = $extracted;
+ $count = count($data[0]);
+
+ $countTwo = count($data);
+ for ($j = 0; $j < $count; $j++) {
+ $args = [];
+ for ($i = 0; $i < $countTwo; $i++) {
+ if (array_key_exists($j, $data[$i])) {
+ $args[] = $data[$i][$j];
+ }
+ }
+ $out[] = vsprintf($format, $args);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Determines if one array contains the exact keys and values of another.
+ *
+ * @param array $data The data to search through.
+ * @param array $needle The values to file in $data
+ * @return bool true If $data contains $needle, false otherwise
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::contains
+ */
+ public static function contains(array $data, array $needle): bool
+ {
+ if (empty($data) || empty($needle)) {
+ return false;
+ }
+ $stack = [];
+
+ while (!empty($needle)) {
+ $key = key($needle);
+ $val = $needle[$key];
+ unset($needle[$key]);
+
+ if (array_key_exists($key, $data) && is_array($val)) {
+ $next = $data[$key];
+ unset($data[$key]);
+
+ if (!empty($val)) {
+ $stack[] = [$val, $next];
+ }
+ } elseif (!array_key_exists($key, $data) || $data[$key] != $val) {
+ return false;
+ }
+
+ if (empty($needle) && !empty($stack)) {
+ [$needle, $data] = array_pop($stack);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Test whether or not a given path exists in $data.
+ * This method uses the same path syntax as Hash::extract()
+ *
+ * Checking for paths that could target more than one element will
+ * make sure that at least one matching element exists.
+ *
+ * @param array $data The data to check.
+ * @param string $path The path to check for.
+ * @return bool Existence of path.
+ * @see \Cake\Utility\Hash::extract()
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::check
+ */
+ public static function check(array $data, string $path): bool
+ {
+ $results = static::extract($data, $path);
+ if (!is_array($results)) {
+ return false;
+ }
+
+ return count($results) > 0;
+ }
+
+ /**
+ * Recursively filters a data set.
+ *
+ * @param array $data Either an array to filter, or value when in callback
+ * @param callable|array $callback A function to filter the data with. Defaults to
+ * `static::_filter()` Which strips out all non-zero empty values.
+ * @return array Filtered array
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::filter
+ */
+ public static function filter(array $data, $callback = ['self', '_filter']): array
+ {
+ foreach ($data as $k => $v) {
+ if (is_array($v)) {
+ $data[$k] = static::filter($v, $callback);
+ }
+ }
+
+ return array_filter($data, $callback);
+ }
+
+ /**
+ * Callback function for filtering.
+ *
+ * @param mixed $var Array to filter.
+ * @return bool
+ */
+ protected static function _filter($var): bool
+ {
+ return $var === 0 || $var === 0.0 || $var === '0' || !empty($var);
+ }
+
+ /**
+ * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
+ * each array element's key, i.e. [['Foo' => ['Bar' => 'Far']]] becomes
+ * ['0.Foo.Bar' => 'Far'].)
+ *
+ * @param array $data Array to flatten
+ * @param string $separator String used to separate array key elements in a path, defaults to '.'
+ * @return array
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::flatten
+ */
+ public static function flatten(array $data, string $separator = '.'): array
+ {
+ $result = [];
+ $stack = [];
+ $path = '';
+
+ reset($data);
+ while (!empty($data)) {
+ $key = key($data);
+ $element = $data[$key];
+ unset($data[$key]);
+
+ if (is_array($element) && !empty($element)) {
+ if (!empty($data)) {
+ $stack[] = [$data, $path];
+ }
+ $data = $element;
+ reset($data);
+ $path .= $key . $separator;
+ } else {
+ $result[$path . $key] = $element;
+ }
+
+ if (empty($data) && !empty($stack)) {
+ [$data, $path] = array_pop($stack);
+ reset($data);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Expands a flat array to a nested array.
+ *
+ * For example, unflattens an array that was collapsed with `Hash::flatten()`
+ * into a multi-dimensional array. So, `['0.Foo.Bar' => 'Far']` becomes
+ * `[['Foo' => ['Bar' => 'Far']]]`.
+ *
+ * @param array $data Flattened array
+ * @param string $separator The delimiter used
+ * @return array
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::expand
+ */
+ public static function expand(array $data, string $separator = '.'): array
+ {
+ $result = [];
+ foreach ($data as $flat => $value) {
+ $keys = explode($separator, (string)$flat);
+ $keys = array_reverse($keys);
+ $child = [
+ $keys[0] => $value,
+ ];
+ array_shift($keys);
+ foreach ($keys as $k) {
+ $child = [
+ $k => $child,
+ ];
+ }
+
+ $stack = [[$child, &$result]];
+ static::_merge($stack, $result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`.
+ *
+ * The difference between this method and the built-in ones, is that if an array key contains another array, then
+ * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for
+ * keys that contain scalar values (unlike `array_merge_recursive`).
+ *
+ * This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays.
+ *
+ * @param array $data Array to be merged
+ * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged
+ * @return array Merged array
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::merge
+ */
+ public static function merge(array $data, $merge): array
+ {
+ $args = array_slice(func_get_args(), 1);
+ $return = $data;
+ $stack = [];
+
+ foreach ($args as &$curArg) {
+ $stack[] = [(array)$curArg, &$return];
+ }
+ unset($curArg);
+ static::_merge($stack, $return);
+
+ return $return;
+ }
+
+ /**
+ * Merge helper function to reduce duplicated code between merge() and expand().
+ *
+ * @param array $stack The stack of operations to work with.
+ * @param array $return The return value to operate on.
+ * @return void
+ */
+ protected static function _merge(array $stack, array &$return): void
+ {
+ while (!empty($stack)) {
+ foreach ($stack as $curKey => &$curMerge) {
+ foreach ($curMerge[0] as $key => &$val) {
+ if (!is_array($curMerge[1])) {
+ continue;
+ }
+
+ if (
+ !empty($curMerge[1][$key])
+ && (array)$curMerge[1][$key] === $curMerge[1][$key]
+ && (array)$val === $val
+ ) {
+ // Recurse into the current merge data as it is an array.
+ $stack[] = [&$val, &$curMerge[1][$key]];
+ } elseif ((int)$key === $key && isset($curMerge[1][$key])) {
+ $curMerge[1][] = $val;
+ } else {
+ $curMerge[1][$key] = $val;
+ }
+ }
+ unset($stack[$curKey]);
+ }
+ unset($curMerge);
+ }
+ }
+
+ /**
+ * Checks to see if all the values in the array are numeric
+ *
+ * @param array $data The array to check.
+ * @return bool true if values are numeric, false otherwise
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::numeric
+ */
+ public static function numeric(array $data): bool
+ {
+ if (empty($data)) {
+ return false;
+ }
+
+ return $data === array_filter($data, 'is_numeric');
+ }
+
+ /**
+ * Counts the dimensions of an array.
+ * Only considers the dimension of the first element in the array.
+ *
+ * If you have an un-even or heterogeneous array, consider using Hash::maxDimensions()
+ * to get the dimensions of the array.
+ *
+ * @param array $data Array to count dimensions on
+ * @return int The number of dimensions in $data
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::dimensions
+ */
+ public static function dimensions(array $data): int
+ {
+ if (empty($data)) {
+ return 0;
+ }
+ reset($data);
+ $depth = 1;
+ while ($elem = array_shift($data)) {
+ if (is_array($elem)) {
+ $depth++;
+ $data = $elem;
+ } else {
+ break;
+ }
+ }
+
+ return $depth;
+ }
+
+ /**
+ * Counts the dimensions of *all* array elements. Useful for finding the maximum
+ * number of dimensions in a mixed array.
+ *
+ * @param array $data Array to count dimensions on
+ * @return int The maximum number of dimensions in $data
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::maxDimensions
+ */
+ public static function maxDimensions(array $data): int
+ {
+ $depth = [];
+ if (!empty($data)) {
+ foreach ($data as $value) {
+ if (is_array($value)) {
+ $depth[] = static::maxDimensions($value) + 1;
+ } else {
+ $depth[] = 1;
+ }
+ }
+ }
+
+ return empty($depth) ? 0 : max($depth);
+ }
+
+ /**
+ * Map a callback across all elements in a set.
+ * Can be provided a path to only modify slices of the set.
+ *
+ * @param array $data The data to map over, and extract data out of.
+ * @param string $path The path to extract for mapping over.
+ * @param callable $function The function to call on each extracted value.
+ * @return array An array of the modified values.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::map
+ */
+ public static function map(array $data, string $path, callable $function): array
+ {
+ $values = (array)static::extract($data, $path);
+
+ return array_map($function, $values);
+ }
+
+ /**
+ * Reduce a set of extracted values using `$function`.
+ *
+ * @param array $data The data to reduce.
+ * @param string $path The path to extract from $data.
+ * @param callable $function The function to call on each extracted value.
+ * @return mixed The reduced value.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::reduce
+ */
+ public static function reduce(array $data, string $path, callable $function)
+ {
+ $values = (array)static::extract($data, $path);
+
+ return array_reduce($values, $function);
+ }
+
+ /**
+ * Apply a callback to a set of extracted values using `$function`.
+ * The function will get the extracted values as the first argument.
+ *
+ * ### Example
+ *
+ * You can easily count the results of an extract using apply().
+ * For example to count the comments on an Article:
+ *
+ * ```
+ * $count = Hash::apply($data, 'Article.Comment.{n}', 'count');
+ * ```
+ *
+ * You could also use a function like `array_sum` to sum the results.
+ *
+ * ```
+ * $total = Hash::apply($data, '{n}.Item.price', 'array_sum');
+ * ```
+ *
+ * @param array $data The data to reduce.
+ * @param string $path The path to extract from $data.
+ * @param callable $function The function to call on each extracted value.
+ * @return mixed The results of the applied method.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::apply
+ */
+ public static function apply(array $data, string $path, callable $function)
+ {
+ $values = (array)static::extract($data, $path);
+
+ return $function($values);
+ }
+
+ /**
+ * Sorts an array by any value, determined by a Set-compatible path
+ *
+ * ### Sort directions
+ *
+ * - `asc` or \SORT_ASC Sort ascending.
+ * - `desc` or \SORT_DESC Sort descending.
+ *
+ * ### Sort types
+ *
+ * - `regular` For regular sorting (don't change types)
+ * - `numeric` Compare values numerically
+ * - `string` Compare values as strings
+ * - `locale` Compare items as strings, based on the current locale
+ * - `natural` Compare items as strings using "natural ordering" in a human friendly way
+ * Will sort foo10 below foo2 as an example.
+ *
+ * To do case insensitive sorting, pass the type as an array as follows:
+ *
+ * ```
+ * Hash::sort($data, 'some.attribute', 'asc', ['type' => 'regular', 'ignoreCase' => true]);
+ * ```
+ *
+ * When using the array form, `type` defaults to 'regular'. The `ignoreCase` option
+ * defaults to `false`.
+ *
+ * @param array $data An array of data to sort
+ * @param string $path A Set-compatible path to the array value
+ * @param string|int $dir See directions above. Defaults to 'asc'.
+ * @param array|string $type See direction types above. Defaults to 'regular'.
+ * @return array Sorted array of data
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::sort
+ */
+ public static function sort(array $data, string $path, $dir = 'asc', $type = 'regular'): array
+ {
+ if (empty($data)) {
+ return [];
+ }
+ $originalKeys = array_keys($data);
+ $numeric = is_numeric(implode('', $originalKeys));
+ if ($numeric) {
+ $data = array_values($data);
+ }
+ /** @var array $sortValues */
+ $sortValues = static::extract($data, $path);
+ $dataCount = count($data);
+
+ // Make sortValues match the data length, as some keys could be missing
+ // the sorted value path.
+ $missingData = count($sortValues) < $dataCount;
+ if ($missingData && $numeric) {
+ // Get the path without the leading '{n}.'
+ $itemPath = substr($path, 4);
+ foreach ($data as $key => $value) {
+ $sortValues[$key] = static::get($value, $itemPath);
+ }
+ } elseif ($missingData) {
+ $sortValues = array_pad($sortValues, $dataCount, null);
+ }
+ $result = static::_squash($sortValues);
+ /** @var array $keys */
+ $keys = static::extract($result, '{n}.id');
+ /** @var array $values */
+ $values = static::extract($result, '{n}.value');
+
+ if (is_string($dir)) {
+ $dir = strtolower($dir);
+ }
+ if (!in_array($dir, [\SORT_ASC, \SORT_DESC], true)) {
+ $dir = $dir === 'asc' ? \SORT_ASC : \SORT_DESC;
+ }
+
+ $ignoreCase = false;
+
+ // $type can be overloaded for case insensitive sort
+ if (is_array($type)) {
+ $type += ['ignoreCase' => false, 'type' => 'regular'];
+ $ignoreCase = $type['ignoreCase'];
+ $type = $type['type'];
+ }
+ $type = strtolower($type);
+
+ if ($type === 'numeric') {
+ $type = \SORT_NUMERIC;
+ } elseif ($type === 'string') {
+ $type = \SORT_STRING;
+ } elseif ($type === 'natural') {
+ $type = \SORT_NATURAL;
+ } elseif ($type === 'locale') {
+ $type = \SORT_LOCALE_STRING;
+ } else {
+ $type = \SORT_REGULAR;
+ }
+ if ($ignoreCase) {
+ $values = array_map('mb_strtolower', $values);
+ }
+ array_multisort($values, $dir, $type, $keys, $dir, $type);
+ $sorted = [];
+ $keys = array_unique($keys);
+
+ foreach ($keys as $k) {
+ if ($numeric) {
+ $sorted[] = $data[$k];
+ continue;
+ }
+ if (isset($originalKeys[$k])) {
+ $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]];
+ } else {
+ $sorted[$k] = $data[$k];
+ }
+ }
+
+ return $sorted;
+ }
+
+ /**
+ * Helper method for sort()
+ * Squashes an array to a single hash so it can be sorted.
+ *
+ * @param array $data The data to squash.
+ * @param mixed $key The key for the data.
+ * @return array
+ */
+ protected static function _squash(array $data, $key = null): array
+ {
+ $stack = [];
+ foreach ($data as $k => $r) {
+ $id = $k;
+ if ($key !== null) {
+ $id = $key;
+ }
+ if (is_array($r) && !empty($r)) {
+ $stack = array_merge($stack, static::_squash($r, $id));
+ } else {
+ $stack[] = ['id' => $id, 'value' => $r];
+ }
+ }
+
+ return $stack;
+ }
+
+ /**
+ * Computes the difference between two complex arrays.
+ * This method differs from the built-in array_diff() in that it will preserve keys
+ * and work on multi-dimensional arrays.
+ *
+ * @param array $data First value
+ * @param array $compare Second value
+ * @return array Returns the key => value pairs that are not common in $data and $compare
+ * The expression for this function is ($data - $compare) + ($compare - ($data - $compare))
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::diff
+ */
+ public static function diff(array $data, array $compare): array
+ {
+ if (empty($data)) {
+ return $compare;
+ }
+ if (empty($compare)) {
+ return $data;
+ }
+ $intersection = array_intersect_key($data, $compare);
+ while (($key = key($intersection)) !== null) {
+ if ($data[$key] == $compare[$key]) {
+ unset($data[$key], $compare[$key]);
+ }
+ next($intersection);
+ }
+
+ return $data + $compare;
+ }
+
+ /**
+ * Merges the difference between $data and $compare onto $data.
+ *
+ * @param array $data The data to append onto.
+ * @param array $compare The data to compare and append onto.
+ * @return array The merged array.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::mergeDiff
+ */
+ public static function mergeDiff(array $data, array $compare): array
+ {
+ if (empty($data) && !empty($compare)) {
+ return $compare;
+ }
+ if (empty($compare)) {
+ return $data;
+ }
+ foreach ($compare as $key => $value) {
+ if (!array_key_exists($key, $data)) {
+ $data[$key] = $value;
+ } elseif (is_array($value) && is_array($data[$key])) {
+ $data[$key] = static::mergeDiff($data[$key], $value);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Normalizes an array, and converts it to a standard format.
+ *
+ * @param array $data List to normalize
+ * @param bool $assoc If true, $data will be converted to an associative array.
+ * @return array
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::normalize
+ */
+ public static function normalize(array $data, bool $assoc = true): array
+ {
+ $keys = array_keys($data);
+ $count = count($keys);
+ $numeric = true;
+
+ if (!$assoc) {
+ for ($i = 0; $i < $count; $i++) {
+ if (!is_int($keys[$i])) {
+ $numeric = false;
+ break;
+ }
+ }
+ }
+ if (!$numeric || $assoc) {
+ $newList = [];
+ for ($i = 0; $i < $count; $i++) {
+ if (is_int($keys[$i])) {
+ $newList[$data[$keys[$i]]] = null;
+ } else {
+ $newList[$keys[$i]] = $data[$keys[$i]];
+ }
+ }
+ $data = $newList;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Takes in a flat array and returns a nested array
+ *
+ * ### Options:
+ *
+ * - `children` The key name to use in the resultset for children.
+ * - `idPath` The path to a key that identifies each entry. Should be
+ * compatible with Hash::extract(). Defaults to `{n}.$alias.id`
+ * - `parentPath` The path to a key that identifies the parent of each entry.
+ * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id`
+ * - `root` The id of the desired top-most result.
+ *
+ * @param array $data The data to nest.
+ * @param array $options Options are:
+ * @return array[] of results, nested
+ * @see \Cake\Utility\Hash::extract()
+ * @throws \InvalidArgumentException When providing invalid data.
+ * @link https://book.cakephp.org/4/en/core-libraries/hash.html#Cake\Utility\Hash::nest
+ */
+ public static function nest(array $data, array $options = []): array
+ {
+ if (!$data) {
+ return $data;
+ }
+
+ $alias = key(current($data));
+ $options += [
+ 'idPath' => "{n}.$alias.id",
+ 'parentPath' => "{n}.$alias.parent_id",
+ 'children' => 'children',
+ 'root' => null,
+ ];
+
+ $return = $idMap = [];
+ /** @var array $ids */
+ $ids = static::extract($data, $options['idPath']);
+
+ $idKeys = explode('.', $options['idPath']);
+ array_shift($idKeys);
+
+ $parentKeys = explode('.', $options['parentPath']);
+ array_shift($parentKeys);
+
+ foreach ($data as $result) {
+ $result[$options['children']] = [];
+
+ $id = static::get($result, $idKeys);
+ $parentId = static::get($result, $parentKeys);
+
+ if (isset($idMap[$id][$options['children']])) {
+ $idMap[$id] = array_merge($result, $idMap[$id]);
+ } else {
+ $idMap[$id] = array_merge($result, [$options['children'] => []]);
+ }
+ if (!$parentId || !in_array($parentId, $ids)) {
+ $return[] = &$idMap[$id];
+ } else {
+ $idMap[$parentId][$options['children']][] = &$idMap[$id];
+ }
+ }
+
+ if (!$return) {
+ throw new InvalidArgumentException('Invalid data array to nest.');
+ }
+
+ if ($options['root']) {
+ $root = $options['root'];
+ } else {
+ $root = static::get($return[0], $parentKeys);
+ }
+
+ foreach ($return as $i => $result) {
+ $id = static::get($result, $idKeys);
+ $parentId = static::get($result, $parentKeys);
+ if ($id !== $root && $parentId != $root) {
+ unset($return[$i]);
+ }
+ }
+
+ return array_values($return);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/Inflector.php b/app/vendor/cakephp/cakephp/src/Utility/Inflector.php
new file mode 100644
index 000000000..73eed4c1d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/Inflector.php
@@ -0,0 +1,523 @@
+ '\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',
+ '/(? '\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
+ */
+ protected static $_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)(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',
+ '/([^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
+ */
+ protected static $_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
+ */
+ protected static $_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
+ */
+ protected static $_cache = [];
+
+ /**
+ * The initial state of Inflector so reset() works.
+ *
+ * @var array
+ */
+ protected static $_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, $value = 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 (empty(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/4/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] = 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/4/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] = 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/4/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/4/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/4/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(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 model class "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/4/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 Cake model class name ("Person" for the database table "people".) for given database table.
+ *
+ * @param string $tableName Name of database table to get class name for
+ * @return string Class name
+ * @link https://book.cakephp.org/4/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/4/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;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Utility/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Utility/MergeVariablesTrait.php b/app/vendor/cakephp/cakephp/src/Utility/MergeVariablesTrait.php
new file mode 100644
index 000000000..803950459
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/MergeVariablesTrait.php
@@ -0,0 +1,118 @@
+{$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 $parentClasses An array of classes you want to merge with.
+ * @param array $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 or not the merging should be done in associative mode.
+ * @return mixed The updated value.
+ */
+ protected function _mergePropertyData(array $current, array $parent, bool $isAssoc)
+ {
+ if (!$isAssoc) {
+ return array_merge($parent, $current);
+ }
+ $parent = Hash::normalize($parent);
+ foreach ($parent as $key => $value) {
+ if (!isset($current[$key])) {
+ $current[$key] = $value;
+ }
+ }
+
+ return $current;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/README.md b/app/vendor/cakephp/cakephp/src/Utility/README.md
new file mode 100644
index 000000000..45601b856
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/README.md
@@ -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/4/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/4/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/4/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/4/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/4/en/core-libraries/xml.html)
diff --git a/app/vendor/cakephp/cakephp/src/Utility/Security.php b/app/vendor/cakephp/cakephp/src/Utility/Security.php
new file mode 100644
index 000000000..5fcdc4fcd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/Security.php
@@ -0,0 +1,310 @@
+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 (empty($cipher)) {
+ throw new InvalidArgumentException('The data to decrypt cannot be empty.');
+ }
+ if ($hmacSalt === null) {
+ $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($original, $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 RuntimeException(
+ '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;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/Text.php b/app/vendor/cakephp/cakephp/src/Utility/Text.php
new file mode 100644
index 000000000..6b1e25232
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/Text.php
@@ -0,0 +1,1187 @@
+ 'Bob', 'age' => '65']);
+ * ```
+ * Returns: Bob is 65 years old.
+ *
+ * Available $options are:
+ *
+ * - before: The character or string in front of the name of the variable placeholder (Defaults to `:`)
+ * - after: The character or string after the name of the variable placeholder (Defaults to null)
+ * - escape: The character or string used to escape the before character / string (Defaults to `\`)
+ * - format: A regex to use for matching variable placeholders. Default is: `/(? val array where each key stands for a placeholder variable name
+ * to be replaced with val
+ * @param array $options An array of options, see description above
+ * @return string
+ */
+ public static function insert(string $str, array $data, array $options = []): string
+ {
+ $defaults = [
+ 'before' => ':', 'after' => '', 'escape' => '\\', 'format' => null, 'clean' => false,
+ ];
+ $options += $defaults;
+ if (empty($data)) {
+ return $options['clean'] ? static::cleanInsert($str, $options) : $str;
+ }
+
+ if (strpos($str, '?') !== false && is_numeric(key($data))) {
+ deprecationWarning(
+ 'Using Text::insert() with `?` placeholders is deprecated. ' .
+ 'Use sprintf() with `%s` placeholders instead.'
+ );
+
+ $offset = 0;
+ while (($pos = strpos($str, '?', $offset)) !== false) {
+ $val = array_shift($data);
+ $offset = $pos + strlen($val);
+ $str = substr_replace($str, $val, $pos, 1);
+ }
+
+ return $options['clean'] ? static::cleanInsert($str, $options) : $str;
+ }
+
+ $format = $options['format'];
+ if ($format === null) {
+ $format = sprintf(
+ '/(? $tempData */
+ $tempData = array_combine($dataKeys, $hashKeys);
+ krsort($tempData);
+
+ foreach ($tempData as $key => $hashVal) {
+ $key = sprintf($format, preg_quote($key, '/'));
+ $str = preg_replace($key, $hashVal, $str);
+ }
+ /** @var array $dataReplacements */
+ $dataReplacements = array_combine($hashKeys, array_values($data));
+ foreach ($dataReplacements as $tmpHash => $tmpValue) {
+ $tmpValue = is_array($tmpValue) ? '' : (string)$tmpValue;
+ $str = str_replace($tmpHash, $tmpValue, $str);
+ }
+
+ if (!isset($options['format']) && isset($options['before'])) {
+ $str = str_replace($options['escape'] . $options['before'], $options['before'], $str);
+ }
+
+ return $options['clean'] ? static::cleanInsert($str, $options) : $str;
+ }
+
+ /**
+ * Cleans up a Text::insert() formatted string with given $options depending on the 'clean' key in
+ * $options. The default method used is text but html is also available. The goal of this function
+ * is to replace all whitespace and unneeded markup around placeholders that did not get replaced
+ * by Text::insert().
+ *
+ * @param string $str String to clean.
+ * @param array $options Options list.
+ * @return string
+ * @see \Cake\Utility\Text::insert()
+ */
+ public static function cleanInsert(string $str, array $options): string
+ {
+ $clean = $options['clean'];
+ if (!$clean) {
+ return $str;
+ }
+ if ($clean === true) {
+ $clean = ['method' => 'text'];
+ }
+ if (!is_array($clean)) {
+ $clean = ['method' => $options['clean']];
+ }
+ switch ($clean['method']) {
+ case 'html':
+ $clean += [
+ 'word' => '[\w,.]+',
+ 'andText' => true,
+ 'replacement' => '',
+ ];
+ $kleenex = sprintf(
+ '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i',
+ preg_quote($options['before'], '/'),
+ $clean['word'],
+ preg_quote($options['after'], '/')
+ );
+ $str = preg_replace($kleenex, $clean['replacement'], $str);
+ if ($clean['andText']) {
+ $options['clean'] = ['method' => 'text'];
+ $str = static::cleanInsert($str, $options);
+ }
+ break;
+ case 'text':
+ $clean += [
+ 'word' => '[\w,.]+',
+ 'gap' => '[\s]*(?:(?:and|or)[\s]*)?',
+ 'replacement' => '',
+ ];
+
+ $kleenex = sprintf(
+ '/(%s%s%s%s|%s%s%s%s)/',
+ preg_quote($options['before'], '/'),
+ $clean['word'],
+ preg_quote($options['after'], '/'),
+ $clean['gap'],
+ $clean['gap'],
+ preg_quote($options['before'], '/'),
+ $clean['word'],
+ preg_quote($options['after'], '/')
+ );
+ $str = preg_replace($kleenex, $clean['replacement'], $str);
+ break;
+ }
+
+ return $str;
+ }
+
+ /**
+ * Wraps text to a specific width, can optionally wrap at word breaks.
+ *
+ * ### Options
+ *
+ * - `width` The width to wrap to. Defaults to 72.
+ * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true.
+ * - `indent` String to indent with. Defaults to null.
+ * - `indentAt` 0 based index to start indenting at. Defaults to 0.
+ *
+ * @param string $text The text to format.
+ * @param array|int $options Array of options to use, or an integer to wrap the text to.
+ * @return string Formatted text.
+ */
+ public static function wrap(string $text, $options = []): string
+ {
+ if (is_numeric($options)) {
+ $options = ['width' => $options];
+ }
+ $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0];
+ if ($options['wordWrap']) {
+ $wrapped = self::wordWrap($text, $options['width'], "\n");
+ } else {
+ $wrapped = trim(chunk_split($text, $options['width'] - 1, "\n"));
+ }
+ if (!empty($options['indent'])) {
+ $chunks = explode("\n", $wrapped);
+ for ($i = $options['indentAt'], $len = count($chunks); $i < $len; $i++) {
+ $chunks[$i] = $options['indent'] . $chunks[$i];
+ }
+ $wrapped = implode("\n", $chunks);
+ }
+
+ return $wrapped;
+ }
+
+ /**
+ * Wraps a complete block of text to a specific width, can optionally wrap
+ * at word breaks.
+ *
+ * ### Options
+ *
+ * - `width` The width to wrap to. Defaults to 72.
+ * - `wordWrap` Only wrap on words breaks (spaces) Defaults to true.
+ * - `indent` String to indent with. Defaults to null.
+ * - `indentAt` 0 based index to start indenting at. Defaults to 0.
+ *
+ * @param string $text The text to format.
+ * @param array|int $options Array of options to use, or an integer to wrap the text to.
+ * @return string Formatted text.
+ */
+ public static function wrapBlock(string $text, $options = []): string
+ {
+ if (is_numeric($options)) {
+ $options = ['width' => $options];
+ }
+ $options += ['width' => 72, 'wordWrap' => true, 'indent' => null, 'indentAt' => 0];
+
+ if (!empty($options['indentAt']) && $options['indentAt'] === 0) {
+ $indentLength = !empty($options['indent']) ? strlen($options['indent']) : 0;
+ $options['width'] -= $indentLength;
+
+ return self::wrap($text, $options);
+ }
+
+ $wrapped = self::wrap($text, $options);
+
+ if (!empty($options['indent'])) {
+ $indentationLength = mb_strlen($options['indent']);
+ $chunks = explode("\n", $wrapped);
+ $count = count($chunks);
+ if ($count < 2) {
+ return $wrapped;
+ }
+ $toRewrap = '';
+ for ($i = $options['indentAt']; $i < $count; $i++) {
+ $toRewrap .= mb_substr($chunks[$i], $indentationLength) . ' ';
+ unset($chunks[$i]);
+ }
+ $options['width'] -= $indentationLength;
+ $options['indentAt'] = 0;
+ $rewrapped = self::wrap($toRewrap, $options);
+ $newChunks = explode("\n", $rewrapped);
+
+ $chunks = array_merge($chunks, $newChunks);
+ $wrapped = implode("\n", $chunks);
+ }
+
+ return $wrapped;
+ }
+
+ /**
+ * Unicode and newline aware version of wordwrap.
+ *
+ * @param string $text The text to format.
+ * @param int $width The width to wrap to. Defaults to 72.
+ * @param string $break The line is broken using the optional break parameter. Defaults to '\n'.
+ * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width.
+ * @return string Formatted text.
+ */
+ public static function wordWrap(string $text, int $width = 72, string $break = "\n", bool $cut = false): string
+ {
+ $paragraphs = explode($break, $text);
+ foreach ($paragraphs as &$paragraph) {
+ $paragraph = static::_wordWrap($paragraph, $width, $break, $cut);
+ }
+
+ return implode($break, $paragraphs);
+ }
+
+ /**
+ * Unicode aware version of wordwrap as helper method.
+ *
+ * @param string $text The text to format.
+ * @param int $width The width to wrap to. Defaults to 72.
+ * @param string $break The line is broken using the optional break parameter. Defaults to '\n'.
+ * @param bool $cut If the cut is set to true, the string is always wrapped at the specified width.
+ * @return string Formatted text.
+ */
+ protected static function _wordWrap(string $text, int $width = 72, string $break = "\n", bool $cut = false): string
+ {
+ if ($cut) {
+ $parts = [];
+ while (mb_strlen($text) > 0) {
+ $part = mb_substr($text, 0, $width);
+ $parts[] = trim($part);
+ $text = trim(mb_substr($text, mb_strlen($part)));
+ }
+
+ return implode($break, $parts);
+ }
+
+ $parts = [];
+ while (mb_strlen($text) > 0) {
+ if ($width >= mb_strlen($text)) {
+ $parts[] = trim($text);
+ break;
+ }
+
+ $part = mb_substr($text, 0, $width);
+ $nextChar = mb_substr($text, $width, 1);
+ if ($nextChar !== ' ') {
+ $breakAt = mb_strrpos($part, ' ');
+ if ($breakAt === false) {
+ $breakAt = mb_strpos($text, ' ', $width);
+ }
+ if ($breakAt === false) {
+ $parts[] = trim($text);
+ break;
+ }
+ $part = mb_substr($text, 0, $breakAt);
+ }
+
+ $part = trim($part);
+ $parts[] = $part;
+ $text = trim(mb_substr($text, mb_strlen($part)));
+ }
+
+ return implode($break, $parts);
+ }
+
+ /**
+ * Highlights a given phrase in a text. You can specify any expression in highlighter that
+ * may include the \1 expression to include the $phrase found.
+ *
+ * ### Options:
+ *
+ * - `format` The piece of HTML with that the phrase will be highlighted
+ * - `html` If true, will ignore any HTML tags, ensuring that only the correct text is highlighted
+ * - `regex` A custom regex rule that is used to match words, default is '|$tag|iu'
+ * - `limit` A limit, optional, defaults to -1 (none)
+ *
+ * @param string $text Text to search the phrase in.
+ * @param string|array $phrase The phrase or phrases that will be searched.
+ * @param array $options An array of HTML attributes and options.
+ * @return string The highlighted text
+ * @link https://book.cakephp.org/4/en/core-libraries/text.html#highlighting-substrings
+ */
+ public static function highlight(string $text, $phrase, array $options = []): string
+ {
+ if (empty($phrase)) {
+ return $text;
+ }
+
+ $defaults = [
+ 'format' => '\1',
+ 'html' => false,
+ 'regex' => '|%s|iu',
+ 'limit' => -1,
+ ];
+ $options += $defaults;
+
+ if (is_array($phrase)) {
+ $replace = [];
+ $with = [];
+
+ foreach ($phrase as $key => $segment) {
+ $segment = '(' . preg_quote($segment, '|') . ')';
+ if ($options['html']) {
+ $segment = "(?![^<]+>)$segment(?![^<]+>)";
+ }
+
+ $with[] = is_array($options['format']) ? $options['format'][$key] : $options['format'];
+ $replace[] = sprintf($options['regex'], $segment);
+ }
+
+ return preg_replace($replace, $with, $text, $options['limit']);
+ }
+
+ $phrase = '(' . preg_quote($phrase, '|') . ')';
+ if ($options['html']) {
+ $phrase = "(?![^<]+>)$phrase(?![^<]+>)";
+ }
+
+ return preg_replace(
+ sprintf($options['regex'], $phrase),
+ $options['format'],
+ $text,
+ $options['limit']
+ );
+ }
+
+ /**
+ * Truncates text starting from the end.
+ *
+ * Cuts a string to the length of $length and replaces the first characters
+ * with the ellipsis if the text is longer than length.
+ *
+ * ### Options:
+ *
+ * - `ellipsis` Will be used as beginning and prepended to the trimmed string
+ * - `exact` If false, $text will not be cut mid-word
+ *
+ * @param string $text String to truncate.
+ * @param int $length Length of returned string, including ellipsis.
+ * @param array $options An array of options.
+ * @return string Trimmed string.
+ */
+ public static function tail(string $text, int $length = 100, array $options = []): string
+ {
+ $default = [
+ 'ellipsis' => '...', 'exact' => true,
+ ];
+ $options += $default;
+ $ellipsis = $options['ellipsis'];
+
+ if (mb_strlen($text) <= $length) {
+ return $text;
+ }
+
+ $truncate = mb_substr($text, mb_strlen($text) - $length + mb_strlen($ellipsis));
+ if (!$options['exact']) {
+ $spacepos = mb_strpos($truncate, ' ');
+ $truncate = $spacepos === false ? '' : trim(mb_substr($truncate, $spacepos));
+ }
+
+ return $ellipsis . $truncate;
+ }
+
+ /**
+ * Truncates text.
+ *
+ * Cuts a string to the length of $length and replaces the last characters
+ * with the ellipsis if the text is longer than length.
+ *
+ * ### Options:
+ *
+ * - `ellipsis` Will be used as ending and appended to the trimmed string
+ * - `exact` If false, $text will not be cut mid-word
+ * - `html` If true, HTML tags would be handled correctly
+ * - `trimWidth` If true, $text will be truncated with the width
+ *
+ * @param string $text String to truncate.
+ * @param int $length Length of returned string, including ellipsis.
+ * @param array $options An array of HTML attributes and options.
+ * @return string Trimmed string.
+ * @link https://book.cakephp.org/4/en/core-libraries/text.html#truncating-text
+ */
+ public static function truncate(string $text, int $length = 100, array $options = []): string
+ {
+ $default = [
+ 'ellipsis' => '...', 'exact' => true, 'html' => false, 'trimWidth' => false,
+ ];
+ if (!empty($options['html']) && strtolower((string)mb_internal_encoding()) === 'utf-8') {
+ $default['ellipsis'] = "\xe2\x80\xa6";
+ }
+ $options += $default;
+
+ $prefix = '';
+ $suffix = $options['ellipsis'];
+
+ if ($options['html']) {
+ $ellipsisLength = self::_strlen(strip_tags($options['ellipsis']), $options);
+
+ $truncateLength = 0;
+ $totalLength = 0;
+ $openTags = [];
+ $truncate = '';
+
+ preg_match_all('/(<\/?([\w+]+)[^>]*>)?([^<>]*)/', $text, $tags, PREG_SET_ORDER);
+ foreach ($tags as $tag) {
+ $contentLength = 0;
+ if (!in_array($tag[2], static::$_defaultHtmlNoCount, true)) {
+ $contentLength = self::_strlen($tag[3], $options);
+ }
+
+ if ($truncate === '') {
+ if (
+ !preg_match(
+ '/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/i',
+ $tag[2]
+ )
+ ) {
+ if (preg_match('/<[\w]+[^>]*>/', $tag[0])) {
+ array_unshift($openTags, $tag[2]);
+ } elseif (preg_match('/<\/([\w]+)[^>]*>/', $tag[0], $closeTag)) {
+ $pos = array_search($closeTag[1], $openTags, true);
+ if ($pos !== false) {
+ array_splice($openTags, $pos, 1);
+ }
+ }
+ }
+
+ $prefix .= $tag[1];
+
+ if ($totalLength + $contentLength + $ellipsisLength > $length) {
+ $truncate = $tag[3];
+ $truncateLength = $length - $totalLength;
+ } else {
+ $prefix .= $tag[3];
+ }
+ }
+
+ $totalLength += $contentLength;
+ if ($totalLength > $length) {
+ break;
+ }
+ }
+
+ if ($totalLength <= $length) {
+ return $text;
+ }
+
+ $text = $truncate;
+ $length = $truncateLength;
+
+ foreach ($openTags as $tag) {
+ $suffix .= '' . $tag . '>';
+ }
+ } else {
+ if (self::_strlen($text, $options) <= $length) {
+ return $text;
+ }
+ $ellipsisLength = self::_strlen($options['ellipsis'], $options);
+ }
+
+ $result = self::_substr($text, 0, $length - $ellipsisLength, $options);
+
+ if (!$options['exact']) {
+ if (self::_substr($text, $length - $ellipsisLength, 1, $options) !== ' ') {
+ $result = self::_removeLastWord($result);
+ }
+
+ // If result is empty, then we don't need to count ellipsis in the cut.
+ if (!strlen($result)) {
+ $result = self::_substr($text, 0, $length, $options);
+ }
+ }
+
+ return $prefix . $result . $suffix;
+ }
+
+ /**
+ * Truncate text with specified width.
+ *
+ * @param string $text String to truncate.
+ * @param int $length Length of returned string, including ellipsis.
+ * @param array $options An array of HTML attributes and options.
+ * @return string Trimmed string.
+ * @see \Cake\Utility\Text::truncate()
+ */
+ public static function truncateByWidth(string $text, int $length = 100, array $options = []): string
+ {
+ return static::truncate($text, $length, ['trimWidth' => true] + $options);
+ }
+
+ /**
+ * Get string length.
+ *
+ * ### Options:
+ *
+ * - `html` If true, HTML entities will be handled as decoded characters.
+ * - `trimWidth` If true, the width will return.
+ *
+ * @param string $text The string being checked for length
+ * @param array $options An array of options.
+ * @return int
+ */
+ protected static function _strlen(string $text, array $options): int
+ {
+ if (empty($options['trimWidth'])) {
+ $strlen = 'mb_strlen';
+ } else {
+ $strlen = 'mb_strwidth';
+ }
+
+ if (empty($options['html'])) {
+ return $strlen($text);
+ }
+
+ $pattern = '/&[0-9a-z]{2,8};|[0-9]{1,7};|[0-9a-f]{1,6};/i';
+ $replace = preg_replace_callback(
+ $pattern,
+ function ($match) use ($strlen) {
+ $utf8 = html_entity_decode($match[0], ENT_HTML5 | ENT_QUOTES, 'UTF-8');
+
+ return str_repeat(' ', $strlen($utf8, 'UTF-8'));
+ },
+ $text
+ );
+
+ return $strlen($replace);
+ }
+
+ /**
+ * Return part of a string.
+ *
+ * ### Options:
+ *
+ * - `html` If true, HTML entities will be handled as decoded characters.
+ * - `trimWidth` If true, will be truncated with specified width.
+ *
+ * @param string $text The input string.
+ * @param int $start The position to begin extracting.
+ * @param int|null $length The desired length.
+ * @param array $options An array of options.
+ * @return string
+ */
+ protected static function _substr(string $text, int $start, ?int $length, array $options): string
+ {
+ if (empty($options['trimWidth'])) {
+ $substr = 'mb_substr';
+ } else {
+ $substr = 'mb_strimwidth';
+ }
+
+ $maxPosition = self::_strlen($text, ['trimWidth' => false] + $options);
+ if ($start < 0) {
+ $start += $maxPosition;
+ if ($start < 0) {
+ $start = 0;
+ }
+ }
+ if ($start >= $maxPosition) {
+ return '';
+ }
+
+ if ($length === null) {
+ $length = self::_strlen($text, $options);
+ }
+
+ if ($length < 0) {
+ $text = self::_substr($text, $start, null, $options);
+ $start = 0;
+ $length += self::_strlen($text, $options);
+ }
+
+ if ($length <= 0) {
+ return '';
+ }
+
+ if (empty($options['html'])) {
+ return (string)$substr($text, $start, $length);
+ }
+
+ $totalOffset = 0;
+ $totalLength = 0;
+ $result = '';
+
+ $pattern = '/(&[0-9a-z]{2,8};|[0-9]{1,7};|[0-9a-f]{1,6};)/i';
+ $parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+ foreach ($parts as $part) {
+ $offset = 0;
+
+ if ($totalOffset < $start) {
+ $len = self::_strlen($part, ['trimWidth' => false] + $options);
+ if ($totalOffset + $len <= $start) {
+ $totalOffset += $len;
+ continue;
+ }
+
+ $offset = $start - $totalOffset;
+ $totalOffset = $start;
+ }
+
+ $len = self::_strlen($part, $options);
+ if ($offset !== 0 || $totalLength + $len > $length) {
+ if (
+ strpos($part, '&') === 0
+ && preg_match($pattern, $part)
+ && $part !== html_entity_decode($part, ENT_HTML5 | ENT_QUOTES, 'UTF-8')
+ ) {
+ // Entities cannot be passed substr.
+ continue;
+ }
+
+ $part = $substr($part, $offset, $length - $totalLength);
+ $len = self::_strlen($part, $options);
+ }
+
+ $result .= $part;
+ $totalLength += $len;
+ if ($totalLength >= $length) {
+ break;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Removes the last word from the input text.
+ *
+ * @param string $text The input text
+ * @return string
+ */
+ protected static function _removeLastWord(string $text): string
+ {
+ $spacepos = mb_strrpos($text, ' ');
+
+ if ($spacepos !== false) {
+ $lastWord = mb_substr($text, $spacepos);
+
+ // Some languages are written without word separation.
+ // We recognize a string as a word if it doesn't contain any full-width characters.
+ if (mb_strwidth($lastWord) === mb_strlen($lastWord)) {
+ $text = mb_substr($text, 0, $spacepos);
+ }
+
+ return $text;
+ }
+
+ return '';
+ }
+
+ /**
+ * Extracts an excerpt from the text surrounding the phrase with a number of characters on each side
+ * determined by radius.
+ *
+ * @param string $text String to search the phrase in
+ * @param string $phrase Phrase that will be searched for
+ * @param int $radius The amount of characters that will be returned on each side of the founded phrase
+ * @param string $ellipsis Ending that will be appended
+ * @return string Modified string
+ * @link https://book.cakephp.org/4/en/core-libraries/text.html#extracting-an-excerpt
+ */
+ public static function excerpt(string $text, string $phrase, int $radius = 100, string $ellipsis = '...'): string
+ {
+ if (empty($text) || empty($phrase)) {
+ return static::truncate($text, $radius * 2, ['ellipsis' => $ellipsis]);
+ }
+
+ $append = $prepend = $ellipsis;
+
+ $phraseLen = mb_strlen($phrase);
+ $textLen = mb_strlen($text);
+
+ $pos = mb_stripos($text, $phrase);
+ if ($pos === false) {
+ return mb_substr($text, 0, $radius) . $ellipsis;
+ }
+
+ $startPos = $pos - $radius;
+ if ($startPos <= 0) {
+ $startPos = 0;
+ $prepend = '';
+ }
+
+ $endPos = $pos + $phraseLen + $radius;
+ if ($endPos >= $textLen) {
+ $endPos = $textLen;
+ $append = '';
+ }
+
+ $excerpt = mb_substr($text, $startPos, $endPos - $startPos);
+ $excerpt = $prepend . $excerpt . $append;
+
+ return $excerpt;
+ }
+
+ /**
+ * Creates a comma separated list where the last two items are joined with 'and', forming natural language.
+ *
+ * @param string[] $list The list to be joined.
+ * @param string|null $and The word used to join the last and second last items together with. Defaults to 'and'.
+ * @param string $separator The separator used to join all the other items together. Defaults to ', '.
+ * @return string The glued together string.
+ * @link https://book.cakephp.org/4/en/core-libraries/text.html#converting-an-array-to-sentence-form
+ */
+ public static function toList(array $list, ?string $and = null, string $separator = ', '): string
+ {
+ if ($and === null) {
+ $and = __d('cake', 'and');
+ }
+ if (count($list) > 1) {
+ return implode($separator, array_slice($list, 0, -1)) . ' ' . $and . ' ' . array_pop($list);
+ }
+
+ return (string)array_pop($list);
+ }
+
+ /**
+ * Check if the string contain multibyte characters
+ *
+ * @param string $string value to test
+ * @return bool
+ */
+ public static function isMultibyte(string $string): bool
+ {
+ $length = strlen($string);
+
+ for ($i = 0; $i < $length; $i++) {
+ $value = ord($string[$i]);
+ if ($value > 128) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Converts a multibyte character string
+ * to the decimal value of the character
+ *
+ * @param string $string String to convert.
+ * @return array
+ */
+ public static function utf8(string $string): array
+ {
+ $map = [];
+
+ $values = [];
+ $find = 1;
+ $length = strlen($string);
+
+ for ($i = 0; $i < $length; $i++) {
+ $value = ord($string[$i]);
+
+ if ($value < 128) {
+ $map[] = $value;
+ } else {
+ if (empty($values)) {
+ $find = $value < 224 ? 2 : 3;
+ }
+ $values[] = $value;
+
+ if (count($values) === $find) {
+ if ($find === 3) {
+ $map[] = (($values[0] % 16) * 4096) + (($values[1] % 64) * 64) + ($values[2] % 64);
+ } else {
+ $map[] = (($values[0] % 32) * 64) + ($values[1] % 64);
+ }
+ $values = [];
+ $find = 1;
+ }
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * Converts the decimal value of a multibyte character string
+ * to a string
+ *
+ * @param array $array Array
+ * @return string
+ */
+ public static function ascii(array $array): string
+ {
+ $ascii = '';
+
+ foreach ($array as $utf8) {
+ if ($utf8 < 128) {
+ $ascii .= chr($utf8);
+ } elseif ($utf8 < 2048) {
+ $ascii .= chr(192 + (($utf8 - ($utf8 % 64)) / 64));
+ $ascii .= chr(128 + ($utf8 % 64));
+ } else {
+ $ascii .= chr(224 + (($utf8 - ($utf8 % 4096)) / 4096));
+ $ascii .= chr(128 + ((($utf8 % 4096) - ($utf8 % 64)) / 64));
+ $ascii .= chr(128 + ($utf8 % 64));
+ }
+ }
+
+ return $ascii;
+ }
+
+ /**
+ * Converts filesize from human readable string to bytes
+ *
+ * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc.
+ * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type'
+ * @return mixed Number of bytes as integer on success, `$default` on failure if not false
+ * @throws \InvalidArgumentException On invalid Unit type.
+ * @link https://book.cakephp.org/4/en/core-libraries/text.html#Cake\Utility\Text::parseFileSize
+ */
+ public static function parseFileSize(string $size, $default = false)
+ {
+ if (ctype_digit($size)) {
+ return (int)$size;
+ }
+ $size = strtoupper($size);
+
+ $l = -2;
+ $i = array_search(substr($size, -2), ['KB', 'MB', 'GB', 'TB', 'PB'], true);
+ if ($i === false) {
+ $l = -1;
+ $i = array_search(substr($size, -1), ['K', 'M', 'G', 'T', 'P'], true);
+ }
+ if ($i !== false) {
+ $size = (float)substr($size, 0, $l);
+
+ return (int)($size * pow(1024, $i + 1));
+ }
+
+ if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) {
+ $size = substr($size, 0, -1);
+
+ return (int)$size;
+ }
+
+ if ($default !== false) {
+ return $default;
+ }
+ throw new InvalidArgumentException('No unit type.');
+ }
+
+ /**
+ * Get the default transliterator.
+ *
+ * @return \Transliterator|null Either a Transliterator instance, or `null`
+ * in case no transliterator has been set yet.
+ */
+ public static function getTransliterator(): ?Transliterator
+ {
+ return static::$_defaultTransliterator;
+ }
+
+ /**
+ * Set the default transliterator.
+ *
+ * @param \Transliterator $transliterator A `Transliterator` instance.
+ * @return void
+ */
+ public static function setTransliterator(Transliterator $transliterator): void
+ {
+ static::$_defaultTransliterator = $transliterator;
+ }
+
+ /**
+ * Get default transliterator identifier string.
+ *
+ * @return string Transliterator identifier.
+ */
+ public static function getTransliteratorId(): string
+ {
+ return static::$_defaultTransliteratorId;
+ }
+
+ /**
+ * Set default transliterator identifier string.
+ *
+ * @param string $transliteratorId Transliterator identifier.
+ * @return void
+ */
+ public static function setTransliteratorId(string $transliteratorId): void
+ {
+ $transliterator = transliterator_create($transliteratorId);
+ if ($transliterator === null) {
+ throw new CakeException('Unable to create transliterator for id: ' . $transliteratorId);
+ }
+
+ static::setTransliterator($transliterator);
+ static::$_defaultTransliteratorId = $transliteratorId;
+ }
+
+ /**
+ * Transliterate string.
+ *
+ * @param string $string String to transliterate.
+ * @param \Transliterator|string|null $transliterator Either a Transliterator
+ * instance, or a transliterator identifier string. If `null`, the default
+ * transliterator (identifier) set via `setTransliteratorId()` or
+ * `setTransliterator()` will be used.
+ * @return string
+ * @see https://secure.php.net/manual/en/transliterator.transliterate.php
+ */
+ public static function transliterate(string $string, $transliterator = null): string
+ {
+ if (empty($transliterator)) {
+ $transliterator = static::$_defaultTransliterator ?: static::$_defaultTransliteratorId;
+ }
+
+ $return = transliterator_transliterate($transliterator, $string);
+ if ($return === false) {
+ throw new CakeException(sprintf('Unable to transliterate string: %s', $string));
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns a string with all spaces converted to dashes (by default),
+ * characters transliterated to ASCII characters, and non word characters removed.
+ *
+ * ### Options:
+ *
+ * - `replacement`: Replacement string. Default '-'.
+ * - `transliteratorId`: A valid transliterator id string.
+ * If `null` (default) the transliterator (identifier) set via
+ * `setTransliteratorId()` or `setTransliterator()` will be used.
+ * If `false` no transliteration will be done, only non words will be removed.
+ * - `preserve`: Specific non-word character to preserve. Default `null`.
+ * For e.g. this option can be set to '.' to generate clean file names.
+ *
+ * @param string $string the string you want to slug
+ * @param array|string $options If string it will be use as replacement character
+ * or an array of options.
+ * @return string
+ * @see setTransliterator()
+ * @see setTransliteratorId()
+ */
+ public static function slug(string $string, $options = []): string
+ {
+ if (is_string($options)) {
+ $options = ['replacement' => $options];
+ }
+ $options += [
+ 'replacement' => '-',
+ 'transliteratorId' => null,
+ 'preserve' => null,
+ ];
+
+ if ($options['transliteratorId'] !== false) {
+ $string = static::transliterate($string, $options['transliteratorId']);
+ }
+
+ $regex = '^\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}';
+ if ($options['preserve']) {
+ $regex .= preg_quote($options['preserve'], '/');
+ }
+ $quotedReplacement = preg_quote((string)$options['replacement'], '/');
+ $map = [
+ '/[' . $regex . ']/mu' => $options['replacement'],
+ sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '',
+ ];
+ if (is_string($options['replacement']) && strlen($options['replacement']) > 0) {
+ $map[sprintf('/[%s]+/mu', $quotedReplacement)] = $options['replacement'];
+ }
+ $string = preg_replace(array_keys($map), $map, $string);
+
+ return $string;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/Xml.php b/app/vendor/cakephp/cakephp/src/Utility/Xml.php
new file mode 100644
index 000000000..a888d825a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/Xml.php
@@ -0,0 +1,500 @@
+text');
+ * ```
+ *
+ * Building XML from string (output DOMDocument):
+ *
+ * ```
+ * $xml = Xml::build('text ', ['return' => 'domdocument']);
+ * ```
+ *
+ * Building XML from a file path:
+ *
+ * ```
+ * $xml = Xml::build('/path/to/an/xml/file.xml');
+ * ```
+ *
+ * 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 ` '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)) {
+ return static::_loadXml(file_get_contents($input), $options);
+ }
+
+ if (!is_string($input)) {
+ $type = gettype($input);
+ throw new XmlException("Invalid input. {$type} cannot be parsed as XML.");
+ }
+
+ if (strpos($input, '<') !== false) {
+ 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 $options The options to use. See Xml::build()
+ * @return \SimpleXMLElement|\DOMDocument
+ * @throws \Cake\Utility\Exception\XmlException
+ */
+ protected static function _loadXml(string $input, array $options)
+ {
+ 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 $options The options to use. See Xml::build()
+ * @return \SimpleXMLElement|\DOMDocument
+ * @throws \Cake\Utility\Exception\XmlException
+ */
+ public static function loadHtml(string $input, array $options = [])
+ {
+ $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') {
+ $xml = 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 $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)
+ {
+ $flags = 0;
+ if (!empty($options['parseHuge'])) {
+ $flags |= LIBXML_PARSEHUGE;
+ }
+
+ $internalErrors = libxml_use_internal_errors(true);
+ if (LIBXML_VERSION < 20900 && !$options['loadEntities']) {
+ $previousDisabledEntityLoader = libxml_disable_entity_loader(true);
+ } elseif ($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 {
+ if (isset($previousDisabledEntityLoader)) {
+ libxml_disable_entity_loader($previousDisabledEntityLoader);
+ }
+ 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:
+ *
+ * `1 defect description `
+ *
+ * And calling `Xml::fromArray($value, 'attributes');` Will generate:
+ *
+ * `description `
+ *
+ * @param array|object $input Array with data or a collection instance.
+ * @param array $options The options to use.
+ * @return \SimpleXMLElement|\DOMDocument SimpleXMLElement or DOMDocument
+ * @throws \Cake\Utility\Exception\XmlException
+ */
+ public static function fromArray($input, array $options = [])
+ {
+ 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.');
+ }
+ $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') {
+ return new SimpleXMLElement($dom->saveXML());
+ }
+
+ return $dom;
+ }
+
+ /**
+ * Recursive method to create children from array
+ *
+ * @param \DOMDocument $dom Handler to DOMDocument
+ * @param \DOMDocument|\DOMElement $node Handler to DOMElement (child)
+ * @param array $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, $node, &$data, $format): void
+ {
+ if (empty($data) || !is_array($data)) {
+ return;
+ }
+ foreach ($data as $key => $value) {
+ if (is_string($key)) {
+ 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 = '';
+ }
+ $isNamespace = strpos($key, 'xmlns:');
+ if ($isNamespace !== false) {
+ /** @psalm-suppress PossiblyUndefinedMethod */
+ $node->setAttributeNS('http://www.w3.org/2000/xmlns/', $key, (string)$value);
+ continue;
+ }
+ if ($key[0] !== '@' && $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, '');
+ $child->appendChild(new DOMText((string)$value));
+ } else {
+ $child = $dom->createElement($key, (string)$value);
+ }
+ $node->appendChild($child);
+ } else {
+ if ($key[0] === '@') {
+ $key = substr($key, 1);
+ }
+ $attribute = $dom->createAttribute($key);
+ $attribute->appendChild($dom->createTextNode((string)$value));
+ $node->appendChild($attribute);
+ }
+ } else {
+ if ($key[0] === '@') {
+ 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 $data Array with information to create children
+ * @return void
+ */
+ protected static function _createChild(array $data): void
+ {
+ $data += [
+ 'dom' => null,
+ 'node' => null,
+ 'key' => null,
+ 'value' => null,
+ 'format' => null,
+ ];
+
+ $value = $data['value'];
+ $dom = $data['dom'];
+ $key = $data['key'];
+ $format = $data['format'];
+ $node = $data['node'];
+
+ $childNS = $childValue = null;
+ 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 (!empty($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|\DOMDocument|\DOMNode $obj SimpleXMLElement, DOMDocument or DOMNode instance
+ * @return array Array representation of the XML structure.
+ * @throws \Cake\Utility\Exception\XmlException
+ */
+ public static function toArray($obj): array
+ {
+ if ($obj instanceof DOMNode) {
+ $obj = simplexml_import_dom($obj);
+ }
+ if (!($obj instanceof SimpleXMLElement)) {
+ throw new XmlException('The input is not instance of SimpleXMLElement, DOMDocument or DOMNode.');
+ }
+ $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 $parentData Parent array with data
+ * @param string $ns Namespace of current child
+ * @param 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) {
+ /** @psalm-suppress PossiblyNullIterator */
+ foreach ($xml->attributes($namespace, true) as $key => $value) {
+ if (!empty($namespace)) {
+ $key = $namespace . ':' . $key;
+ }
+ $data['@' . $key] = (string)$value;
+ }
+
+ foreach ($xml->children($namespace, true) as $child) {
+ /** @psalm-suppress PossiblyNullArgument */
+ static::_toArray($child, $data, $namespace, $namespaces);
+ }
+ }
+
+ $asString = trim((string)$xml);
+ if (empty($data)) {
+ $data = $asString;
+ } elseif (strlen($asString) > 0) {
+ $data['@'] = $asString;
+ }
+
+ if (!empty($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;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Utility/bootstrap.php b/app/vendor/cakephp/cakephp/src/Utility/bootstrap.php
new file mode 100644
index 000000000..d335b754d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Utility/bootstrap.php
@@ -0,0 +1,21 @@
+=7.2.0",
+ "cakephp/core": "^4.0"
+ },
+ "suggest": {
+ "ext-intl": "To use Text::transliterate() or Text::slug()",
+ "lib-ICU": "To use Text::transliterate() or Text::slug()"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Utility\\": "."
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/LICENSE.txt b/app/vendor/cakephp/cakephp/src/Validation/LICENSE.txt
new file mode 100644
index 000000000..b938c9e8e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/LICENSE.txt
@@ -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.
diff --git a/app/vendor/cakephp/cakephp/src/Validation/README.md b/app/vendor/cakephp/cakephp/src/Validation/README.md
new file mode 100644
index 000000000..d3484fac3
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/README.md
@@ -0,0 +1,37 @@
+[](https://packagist.org/packages/cakephp/validation)
+[](LICENSE.txt)
+
+# CakePHP Validation Library
+
+The validation library in CakePHP provides features to build validators that can validate arbitrary
+arrays of data with ease.
+
+## Usage
+
+Validator objects define the rules that apply to a set of fields. Validator objects contain a mapping between
+fields and validation sets. Creating a validator is simple:
+
+```php
+use Cake\Validation\Validator;
+
+$validator = new Validator();
+$validator
+ ->requirePresence('email')
+ ->add('email', 'validFormat', [
+ 'rule' => 'email',
+ 'message' => 'E-mail must be valid'
+ ])
+ ->requirePresence('name')
+ ->notEmptyString('name', 'We need your name.')
+ ->requirePresence('comment')
+ ->notEmptyString('comment', 'You need to give a comment.');
+
+$errors = $validator->validate($_POST);
+if (!empty($errors)) {
+ // display errors.
+}
+```
+
+## Documentation
+
+Please make sure you check the [official documentation](https://book.cakephp.org/4/en/core-libraries/validation.html)
diff --git a/app/vendor/cakephp/cakephp/src/Validation/RulesProvider.php b/app/vendor/cakephp/cakephp/src/Validation/RulesProvider.php
new file mode 100644
index 000000000..843f03255
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/RulesProvider.php
@@ -0,0 +1,79 @@
+_class = $class;
+ $this->_reflection = new ReflectionClass($class);
+ }
+
+ /**
+ * Proxies validation method calls to the Validation class.
+ *
+ * The last argument (context) will be sliced off, if the validation
+ * method's last parameter is not named 'context'. This lets
+ * the various wrapped validation methods to not receive the validation
+ * context unless they need it.
+ *
+ * @param string $method the validation method to call
+ * @param array $arguments the list of arguments to pass to the method
+ * @return bool Whether or not the validation rule passed
+ */
+ public function __call(string $method, array $arguments)
+ {
+ $method = $this->_reflection->getMethod($method);
+ $argumentList = $method->getParameters();
+ if (array_pop($argumentList)->getName() !== 'context') {
+ $arguments = array_slice($arguments, 0, -1);
+ }
+ $object = is_string($this->_class) ? null : $this->_class;
+
+ return $method->invokeArgs($object, $arguments);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidatableInterface.php b/app/vendor/cakephp/cakephp/src/Validation/ValidatableInterface.php
new file mode 100644
index 000000000..9defaa34a
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/ValidatableInterface.php
@@ -0,0 +1,32 @@
+';
+
+ /**
+ * Greater than or equal to comparison operator.
+ *
+ * @var string
+ */
+ public const COMPARE_GREATER_OR_EQUAL = '>=';
+
+ /**
+ * Less than comparison operator.
+ *
+ * @var string
+ */
+ public const COMPARE_LESS = '<';
+
+ /**
+ * Less than or equal to comparison operator.
+ *
+ * @var string
+ */
+ public const COMPARE_LESS_OR_EQUAL = '<=';
+
+ /**
+ * @var string[]
+ */
+ protected const COMPARE_STRING = [
+ self::COMPARE_EQUAL,
+ self::COMPARE_NOT_EQUAL,
+ self::COMPARE_SAME,
+ self::COMPARE_NOT_SAME,
+ ];
+
+ /**
+ * Datetime ISO8601 format
+ *
+ * @var string
+ */
+ public const DATETIME_ISO8601 = 'iso8601';
+
+ /**
+ * Some complex patterns needed in multiple places
+ *
+ * @var array
+ */
+ protected static $_pattern = [
+ 'hostname' => '(?:[_\p{L}0-9][-_\p{L}0-9]*\.)*(?:[\p{L}0-9][-\p{L}0-9]{0,62})\.(?:(?:[a-z]{2}\.)?[a-z]{2,})',
+ 'latitude' => '[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)',
+ 'longitude' => '[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)',
+ ];
+
+ /**
+ * Holds an array of errors messages set in this class.
+ * These are used for debugging purposes
+ *
+ * @var array
+ */
+ public static $errors = [];
+
+ /**
+ * Checks that a string contains something other than whitespace
+ *
+ * Returns true if string contains something other than whitespace
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function notBlank($check): bool
+ {
+ if (empty($check) && !is_bool($check) && !is_numeric($check)) {
+ return false;
+ }
+
+ return static::_check($check, '/[^\s]+/m');
+ }
+
+ /**
+ * Checks that a string contains only integer or letters.
+ *
+ * This method's definition of letters and integers includes unicode characters.
+ * Use `asciiAlphaNumeric()` if you want to exclude unicode.
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function alphaNumeric($check): bool
+ {
+ if ((empty($check) && $check !== '0') || !is_scalar($check)) {
+ return false;
+ }
+
+ return self::_check($check, '/^[\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]+$/Du');
+ }
+
+ /**
+ * Checks that a doesn't contain any alpha numeric characters
+ *
+ * This method's definition of letters and integers includes unicode characters.
+ * Use `notAsciiAlphaNumeric()` if you want to exclude ascii only.
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function notAlphaNumeric($check): bool
+ {
+ return !static::alphaNumeric($check);
+ }
+
+ /**
+ * Checks that a string contains only ascii integer or letters.
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function asciiAlphaNumeric($check): bool
+ {
+ if ((empty($check) && $check !== '0') || !is_scalar($check)) {
+ return false;
+ }
+
+ return self::_check($check, '/^[[:alnum:]]+$/');
+ }
+
+ /**
+ * Checks that a doesn't contain any non-ascii alpha numeric characters
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function notAsciiAlphaNumeric($check): bool
+ {
+ return !static::asciiAlphaNumeric($check);
+ }
+
+ /**
+ * Checks that a string length is within specified range.
+ * Spaces are included in the character count.
+ * Returns true if string matches value min, max, or between min and max,
+ *
+ * @param mixed $check Value to check for length
+ * @param int $min Minimum value in range (inclusive)
+ * @param int $max Maximum value in range (inclusive)
+ * @return bool Success
+ */
+ public static function lengthBetween($check, int $min, int $max): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+ $length = mb_strlen((string)$check);
+
+ return $length >= $min && $length <= $max;
+ }
+
+ /**
+ * Validation of credit card numbers.
+ * Returns true if $check is in the proper credit card format.
+ *
+ * @param mixed $check credit card number to validate
+ * @param string|string[] $type 'all' may be passed as a string, defaults to fast which checks format of
+ * most major credit cards if an array is used only the values of the array are checked.
+ * Example: ['amex', 'bankcard', 'maestro']
+ * @param bool $deep set to true this will check the Luhn algorithm of the credit card.
+ * @param string|null $regex A custom regex, this will be used instead of the defined regex values.
+ * @return bool Success
+ * @see \Cake\Validation\Validation::luhn()
+ */
+ public static function creditCard($check, $type = 'fast', bool $deep = false, ?string $regex = null): bool
+ {
+ if (!(is_string($check) || is_int($check))) {
+ return false;
+ }
+
+ $check = str_replace(['-', ' '], '', (string)$check);
+ if (mb_strlen($check) < 13) {
+ return false;
+ }
+
+ if ($regex !== null && static::_check($check, $regex)) {
+ return !$deep || static::luhn($check);
+ }
+ $cards = [
+ 'all' => [
+ 'amex' => '/^3[47]\\d{13}$/',
+ 'bankcard' => '/^56(10\\d\\d|022[1-5])\\d{10}$/',
+ 'diners' => '/^(?:3(0[0-5]|[68]\\d)\\d{11})|(?:5[1-5]\\d{14})$/',
+ 'disc' => '/^(?:6011|650\\d)\\d{12}$/',
+ 'electron' => '/^(?:417500|4917\\d{2}|4913\\d{2})\\d{10}$/',
+ 'enroute' => '/^2(?:014|149)\\d{11}$/',
+ 'jcb' => '/^(3\\d{4}|2131|1800)\\d{11}$/',
+ 'maestro' => '/^(?:5020|6\\d{3})\\d{12}$/',
+ 'mc' => '/^(5[1-5]\\d{14})|(2(?:22[1-9]|2[3-9][0-9]|[3-6][0-9]{2}|7[0-1][0-9]|720)\\d{12})$/',
+ 'solo' => '/^(6334[5-9][0-9]|6767[0-9]{2})\\d{10}(\\d{2,3})?$/',
+ // phpcs:ignore Generic.Files.LineLength
+ 'switch' => '/^(?:49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})\\d{10}(\\d{2,3})?)|(?:564182\\d{10}(\\d{2,3})?)|(6(3(33[0-4][0-9])|759[0-9]{2})\\d{10}(\\d{2,3})?)$/',
+ 'visa' => '/^4\\d{12}(\\d{3})?$/',
+ 'voyager' => '/^8699[0-9]{11}$/',
+ ],
+ // phpcs:ignore Generic.Files.LineLength
+ 'fast' => '/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|3(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})$/',
+ ];
+
+ if (is_array($type)) {
+ foreach ($type as $value) {
+ $regex = $cards['all'][strtolower($value)];
+
+ if (static::_check($check, $regex)) {
+ return static::luhn($check);
+ }
+ }
+ } elseif ($type === 'all') {
+ foreach ($cards['all'] as $value) {
+ $regex = $value;
+
+ if (static::_check($check, $regex)) {
+ return static::luhn($check);
+ }
+ }
+ } else {
+ $regex = $cards['fast'];
+
+ if (static::_check($check, $regex)) {
+ return static::luhn($check);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Used to check the count of a given value of type array or Countable.
+ *
+ * @param mixed $check The value to check the count on.
+ * @param string $operator Can be either a word or operand
+ * is greater >, is less <, greater or equal >=
+ * less or equal <=, is less <, equal to ==, not equal !=
+ * @param int $expectedCount The expected count value.
+ * @return bool Success
+ */
+ public static function numElements($check, string $operator, int $expectedCount): bool
+ {
+ if (!is_array($check) && !$check instanceof Countable) {
+ return false;
+ }
+
+ return self::comparison(count($check), $operator, $expectedCount);
+ }
+
+ /**
+ * Used to compare 2 numeric values.
+ *
+ * @param string|int $check1 The left value to compare.
+ * @param string $operator Can be one of following operator strings:
+ * '>', '<', '>=', '<=', '==', '!=', '===' and '!=='. You can use one of
+ * the Validation::COMPARE_* constants.
+ * @param string|int $check2 The right value to compare.
+ * @return bool Success
+ */
+ public static function comparison($check1, string $operator, $check2): bool
+ {
+ if (
+ (!is_numeric($check1) || !is_numeric($check2)) &&
+ !in_array($operator, static::COMPARE_STRING)
+ ) {
+ return false;
+ }
+
+ switch ($operator) {
+ case static::COMPARE_GREATER:
+ if ($check1 > $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_LESS:
+ if ($check1 < $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_GREATER_OR_EQUAL:
+ if ($check1 >= $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_LESS_OR_EQUAL:
+ if ($check1 <= $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_EQUAL:
+ if ($check1 == $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_NOT_EQUAL:
+ if ($check1 != $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_SAME:
+ if ($check1 === $check2) {
+ return true;
+ }
+ break;
+ case static::COMPARE_NOT_SAME:
+ if ($check1 !== $check2) {
+ return true;
+ }
+ break;
+ default:
+ static::$errors[] = 'You must define a valid $operator parameter for Validation::comparison()';
+ }
+
+ return false;
+ }
+
+ /**
+ * Compare one field to another.
+ *
+ * If both fields have exactly the same value this method will return true.
+ *
+ * @param mixed $check The value to find in $field.
+ * @param string $field The field to check $check against. This field must be present in $context.
+ * @param array $context The validation context.
+ * @return bool
+ */
+ public static function compareWith($check, string $field, array $context): bool
+ {
+ return self::compareFields($check, $field, static::COMPARE_SAME, $context);
+ }
+
+ /**
+ * Compare one field to another.
+ *
+ * Return true if the comparison matches the expected result.
+ *
+ * @param mixed $check The value to find in $field.
+ * @param string $field The field to check $check against. This field must be present in $context.
+ * @param string $operator Comparison operator. See Validation::comparison().
+ * @param array $context The validation context.
+ * @return bool
+ * @since 3.6.0
+ */
+ public static function compareFields($check, string $field, string $operator, array $context): bool
+ {
+ if (!isset($context['data']) || !array_key_exists($field, $context['data'])) {
+ return false;
+ }
+
+ return static::comparison($check, $operator, $context['data'][$field]);
+ }
+
+ /**
+ * Checks if a string contains one or more non-alphanumeric characters.
+ *
+ * Returns true if string contains at least the specified number of non-alphanumeric characters
+ *
+ * @param mixed $check Value to check
+ * @param int $count Number of non-alphanumerics to check for
+ * @return bool Success
+ * @deprecated 4.0.0 Use {@link notAlphaNumeric()} instead. Will be removed in 5.0
+ */
+ public static function containsNonAlphaNumeric($check, int $count = 1): bool
+ {
+ deprecationWarning('Validation::containsNonAlphaNumeric() is deprecated. Use notAlphaNumeric() instead.');
+ if (!is_string($check)) {
+ return false;
+ }
+
+ $matches = preg_match_all('/[^a-zA-Z0-9]/', $check);
+
+ return $matches >= $count;
+ }
+
+ /**
+ * Used when a custom regular expression is needed.
+ *
+ * @param mixed $check The value to check.
+ * @param string|null $regex If $check is passed as a string, $regex must also be set to valid regular expression
+ * @return bool Success
+ */
+ public static function custom($check, ?string $regex = null): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+ if ($regex === null) {
+ static::$errors[] = 'You must define a regular expression for Validation::custom()';
+
+ return false;
+ }
+
+ return static::_check($check, $regex);
+ }
+
+ /**
+ * Date validation, determines if the string passed is a valid date.
+ * keys that expect full month, day and year will validate leap years.
+ *
+ * Years are valid from 0001 to 2999.
+ *
+ * ### Formats:
+ *
+ * - `dmy` 27-12-2006 or 27-12-06 separators can be a space, period, dash, forward slash
+ * - `mdy` 12-27-2006 or 12-27-06 separators can be a space, period, dash, forward slash
+ * - `ymd` 2006-12-27 or 06-12-27 separators can be a space, period, dash, forward slash
+ * - `dMy` 27 December 2006 or 27 Dec 2006
+ * - `Mdy` December 27, 2006 or Dec 27, 2006 comma is optional
+ * - `My` December 2006 or Dec 2006
+ * - `my` 12/2006 or 12/06 separators can be a space, period, dash, forward slash
+ * - `ym` 2006/12 or 06/12 separators can be a space, period, dash, forward slash
+ * - `y` 2006 just the year without any separators
+ *
+ * @param mixed $check a valid date string/object
+ * @param string|array $format Use a string or an array of the keys above.
+ * Arrays should be passed as ['dmy', 'mdy', etc]
+ * @param string|null $regex If a custom regular expression is used this is the only validation that will occur.
+ * @return bool Success
+ */
+ public static function date($check, $format = 'ymd', ?string $regex = null): bool
+ {
+ if ($check instanceof DateTimeInterface) {
+ return true;
+ }
+ if (is_object($check)) {
+ return false;
+ }
+ if (is_array($check)) {
+ $check = static::_getDateString($check);
+ $format = 'ymd';
+ }
+
+ if ($regex !== null) {
+ return static::_check($check, $regex);
+ }
+ $month = '(0[123456789]|10|11|12)';
+ $separator = '([- /.])';
+ // Don't allow 0000, but 0001-2999 are ok.
+ $fourDigitYear = '(?:(?!0000)[012]\d{3})';
+ $twoDigitYear = '(?:\d{2})';
+ $year = '(?:' . $fourDigitYear . '|' . $twoDigitYear . ')';
+
+ // phpcs:disable Generic.Files.LineLength
+ // 2 or 4 digit leap year sub-pattern
+ $leapYear = '(?:(?:(?:(?!0000)[012]\\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)))';
+ // 4 digit leap year sub-pattern
+ $fourDigitLeapYear = '(?:(?:(?:(?!0000)[012]\\d)(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00)))';
+
+ $regex['dmy'] = '%^(?:(?:31(\\/|-|\\.|\\x20)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)' .
+ $separator . '(?:0?[13-9]|1[0-2])\\2))' . $year . '$|^(?:29' .
+ $separator . '0?2\\3' . $leapYear . ')$|^(?:0?[1-9]|1\\d|2[0-8])' .
+ $separator . '(?:(?:0?[1-9])|(?:1[0-2]))\\4' . $year . '$%';
+
+ $regex['mdy'] = '%^(?:(?:(?:0?[13578]|1[02])(\\/|-|\\.|\\x20)31)\\1|(?:(?:0?[13-9]|1[0-2])' .
+ $separator . '(?:29|30)\\2))' . $year . '$|^(?:0?2' . $separator . '29\\3' . $leapYear . ')$|^(?:(?:0?[1-9])|(?:1[0-2]))' .
+ $separator . '(?:0?[1-9]|1\\d|2[0-8])\\4' . $year . '$%';
+
+ $regex['ymd'] = '%^(?:(?:' . $leapYear .
+ $separator . '(?:0?2\\1(?:29)))|(?:' . $year .
+ $separator . '(?:(?:(?:0?[13578]|1[02])\\2(?:31))|(?:(?:0?[13-9]|1[0-2])\\2(29|30))|(?:(?:0?[1-9])|(?:1[0-2]))\\2(?:0?[1-9]|1\\d|2[0-8]))))$%';
+
+ $regex['dMy'] = '/^((31(?!\\ (Feb(ruary)?|Apr(il)?|June?|(Sep(?=\\b|t)t?|Nov)(ember)?)))|((30|29)(?!\\ Feb(ruary)?))|(29(?=\\ Feb(ruary)?\\ ' . $fourDigitLeapYear . '))|(0?[1-9])|1\\d|2[0-8])\\ (Jan(uary)?|Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)\\ ' . $fourDigitYear . '$/';
+
+ $regex['Mdy'] = '/^(?:(((Jan(uary)?|Ma(r(ch)?|y)|Jul(y)?|Aug(ust)?|Oct(ober)?|Dec(ember)?)\\ 31)|((Jan(uary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|Oct(ober)?|(Sep)(tember)?|(Nov|Dec)(ember)?)\\ (0?[1-9]|([12]\\d)|30))|(Feb(ruary)?\\ (0?[1-9]|1\\d|2[0-8]|(29(?=,?\\ ' . $fourDigitLeapYear . ')))))\\,?\\ ' . $fourDigitYear . ')$/';
+
+ $regex['My'] = '%^(Jan(uary)?|Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)' .
+ $separator . $fourDigitYear . '$%';
+ // phpcs:enable Generic.Files.LineLength
+
+ $regex['my'] = '%^(' . $month . $separator . $year . ')$%';
+ $regex['ym'] = '%^(' . $year . $separator . $month . ')$%';
+ $regex['y'] = '%^(' . $fourDigitYear . ')$%';
+
+ $format = is_array($format) ? array_values($format) : [$format];
+ foreach ($format as $key) {
+ if (static::_check($check, $regex[$key]) === true) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Validates a datetime value
+ *
+ * All values matching the "date" core validation rule, and the "time" one will be valid
+ *
+ * @param mixed $check Value to check
+ * @param string|array $dateFormat Format of the date part. See Validation::date() for more information.
+ * Or `Validation::DATETIME_ISO8601` to validate an ISO8601 datetime value.
+ * @param string|null $regex Regex for the date part. If a custom regular expression is used
+ * this is the only validation that will occur.
+ * @return bool True if the value is valid, false otherwise
+ * @see \Cake\Validation\Validation::date()
+ * @see \Cake\Validation\Validation::time()
+ */
+ public static function datetime($check, $dateFormat = 'ymd', ?string $regex = null): bool
+ {
+ if ($check instanceof DateTimeInterface) {
+ return true;
+ }
+ if (is_object($check)) {
+ return false;
+ }
+ if (is_array($dateFormat) && count($dateFormat) === 1) {
+ $dateFormat = reset($dateFormat);
+ }
+ if ($dateFormat === static::DATETIME_ISO8601 && !static::iso8601($check)) {
+ return false;
+ }
+
+ $valid = false;
+ if (is_array($check)) {
+ $check = static::_getDateString($check);
+ $dateFormat = 'ymd';
+ }
+ $parts = preg_split('/[\sT]+/', $check);
+ if (!empty($parts) && count($parts) > 1) {
+ $date = rtrim(array_shift($parts), ',');
+ $time = implode(' ', $parts);
+ if ($dateFormat === static::DATETIME_ISO8601) {
+ $dateFormat = 'ymd';
+ $time = preg_split("/[TZ\-\+\.]/", $time);
+ $time = array_shift($time);
+ }
+ $valid = static::date($date, $dateFormat, $regex) && static::time($time);
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Validates an iso8601 datetime format
+ * ISO8601 recognize datetime like 2019 as a valid date. To validate and check date integrity, use @see \Cake\Validation\Validation::datetime()
+ *
+ * @param mixed $check Value to check
+ * @return bool True if the value is valid, false otherwise
+ * @see Regex credits: https://www.myintervals.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
+ */
+ public static function iso8601($check): bool
+ {
+ if ($check instanceof DateTimeInterface) {
+ return true;
+ }
+ if (is_object($check)) {
+ return false;
+ }
+
+ // phpcs:ignore Generic.Files.LineLength
+ $regex = '/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/';
+
+ return static::_check($check, $regex);
+ }
+
+ /**
+ * Time validation, determines if the string passed is a valid time.
+ * Validates time as 24hr (HH:MM[:SS][.FFFFFF]) or am/pm ([H]H:MM[a|p]m)
+ *
+ * Seconds and fractional seconds (microseconds) are allowed but optional
+ * in 24hr format.
+ *
+ * @param mixed $check a valid time string/object
+ * @return bool Success
+ */
+ public static function time($check): bool
+ {
+ if ($check instanceof DateTimeInterface) {
+ return true;
+ }
+ if (is_array($check)) {
+ $check = static::_getDateString($check);
+ }
+
+ if (!is_scalar($check)) {
+ return false;
+ }
+
+ $meridianClockRegex = '^((0?[1-9]|1[012])(:[0-5]\d){0,2} ?([AP]M|[ap]m))$';
+ $standardClockRegex = '^([01]\d|2[0-3])((:[0-5]\d){1,2}|(:[0-5]\d){2}\.\d{0,6})$';
+
+ return static::_check($check, '%' . $meridianClockRegex . '|' . $standardClockRegex . '%');
+ }
+
+ /**
+ * Date and/or time string validation.
+ * Uses `I18n::Time` to parse the date. This means parsing is locale dependent.
+ *
+ * @param mixed $check a date string or object (will always pass)
+ * @param string $type Parser type, one out of 'date', 'time', and 'datetime'
+ * @param string|int|null $format any format accepted by IntlDateFormatter
+ * @return bool Success
+ * @throws \InvalidArgumentException when unsupported $type given
+ * @see \Cake\I18n\Time::parseDate()
+ * @see \Cake\I18n\Time::parseTime()
+ * @see \Cake\I18n\Time::parseDateTime()
+ */
+ public static function localizedTime($check, string $type = 'datetime', $format = null): bool
+ {
+ if ($check instanceof DateTimeInterface) {
+ return true;
+ }
+ if (!is_string($check)) {
+ return false;
+ }
+ static $methods = [
+ 'date' => 'parseDate',
+ 'time' => 'parseTime',
+ 'datetime' => 'parseDateTime',
+ ];
+ if (empty($methods[$type])) {
+ throw new InvalidArgumentException('Unsupported parser type given.');
+ }
+ $method = $methods[$type];
+
+ return Time::$method($check, $format) !== null;
+ }
+
+ /**
+ * Validates if passed value is boolean-like.
+ *
+ * The list of what is considered to be boolean values, may be set via $booleanValues.
+ *
+ * @param bool|int|string $check Value to check.
+ * @param array $booleanValues List of valid boolean values, defaults to `[true, false, 0, 1, '0', '1']`.
+ * @return bool Success.
+ */
+ public static function boolean($check, array $booleanValues = []): bool
+ {
+ if (!$booleanValues) {
+ $booleanValues = [true, false, 0, 1, '0', '1'];
+ }
+
+ return in_array($check, $booleanValues, true);
+ }
+
+ /**
+ * Validates if given value is truthy.
+ *
+ * The list of what is considered to be truthy values, may be set via $truthyValues.
+ *
+ * @param bool|int|string $check Value to check.
+ * @param array $truthyValues List of valid truthy values, defaults to `[true, 1, '1']`.
+ * @return bool Success.
+ */
+ public static function truthy($check, array $truthyValues = []): bool
+ {
+ if (!$truthyValues) {
+ $truthyValues = [true, 1, '1'];
+ }
+
+ return in_array($check, $truthyValues, true);
+ }
+
+ /**
+ * Validates if given value is falsey.
+ *
+ * The list of what is considered to be falsey values, may be set via $falseyValues.
+ *
+ * @param bool|int|string $check Value to check.
+ * @param array $falseyValues List of valid falsey values, defaults to `[false, 0, '0']`.
+ * @return bool Success.
+ */
+ public static function falsey($check, array $falseyValues = []): bool
+ {
+ if (!$falseyValues) {
+ $falseyValues = [false, 0, '0'];
+ }
+
+ return in_array($check, $falseyValues, true);
+ }
+
+ /**
+ * Checks that a value is a valid decimal. Both the sign and exponent are optional.
+ *
+ * Valid Places:
+ *
+ * - null => Any number of decimal places, including none. The '.' is not required.
+ * - true => Any number of decimal places greater than 0, or a float|double. The '.' is required.
+ * - 1..N => Exactly that many number of decimal places. The '.' is required.
+ *
+ * @param mixed $check The value the test for decimal.
+ * @param int|true|null $places Decimal places.
+ * @param string|null $regex If a custom regular expression is used, this is the only validation that will occur.
+ * @return bool Success
+ */
+ public static function decimal($check, $places = null, ?string $regex = null): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+
+ if ($regex === null) {
+ $lnum = '[0-9]+';
+ $dnum = "[0-9]*[\.]{$lnum}";
+ $sign = '[+-]?';
+ $exp = "(?:[eE]{$sign}{$lnum})?";
+
+ if ($places === null) {
+ $regex = "/^{$sign}(?:{$lnum}|{$dnum}){$exp}$/";
+ } elseif ($places === true) {
+ if (is_float($check) && floor($check) === $check) {
+ $check = sprintf('%.1f', $check);
+ }
+ $regex = "/^{$sign}{$dnum}{$exp}$/";
+ } elseif (is_numeric($places)) {
+ $places = '[0-9]{' . $places . '}';
+ $dnum = "(?:[0-9]*[\.]{$places}|{$lnum}[\.]{$places})";
+ $regex = "/^{$sign}{$dnum}{$exp}$/";
+ } else {
+ return false;
+ }
+ }
+
+ // account for localized floats.
+ $locale = ini_get('intl.default_locale') ?: static::DEFAULT_LOCALE;
+ $formatter = new NumberFormatter($locale, NumberFormatter::DECIMAL);
+ $decimalPoint = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
+ $groupingSep = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
+
+ // There are two types of non-breaking spaces - we inject a space to account for human input
+ if ($groupingSep == "\xc2\xa0" || $groupingSep == "\xe2\x80\xaf") {
+ $check = str_replace([' ', $groupingSep, $decimalPoint], ['', '', '.'], (string)$check);
+ } else {
+ $check = str_replace([$groupingSep, $decimalPoint], ['', '.'], (string)$check);
+ }
+
+ return static::_check($check, $regex);
+ }
+
+ /**
+ * Validates for an email address.
+ *
+ * Only uses getmxrr() checking for deep validation, or
+ * any PHP version on a non-windows distribution
+ *
+ * @param mixed $check Value to check
+ * @param bool $deep Perform a deeper validation (if true), by also checking availability of host
+ * @param string|null $regex Regex to use (if none it will use built in regex)
+ * @return bool Success
+ */
+ public static function email($check, ?bool $deep = false, ?string $regex = null): bool
+ {
+ if (!is_string($check)) {
+ return false;
+ }
+
+ if ($regex === null) {
+ // phpcs:ignore Generic.Files.LineLength
+ $regex = '/^[\p{L}0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[\p{L}0-9!#$%&\'*+\/=?^_`{|}~-]+)*@' . self::$_pattern['hostname'] . '$/ui';
+ }
+ $return = static::_check($check, $regex);
+ if ($deep === false || $deep === null) {
+ return $return;
+ }
+
+ if ($return === true && preg_match('/@(' . static::$_pattern['hostname'] . ')$/i', $check, $regs)) {
+ if (function_exists('getmxrr') && getmxrr($regs[1], $mxhosts)) {
+ return true;
+ }
+ if (function_exists('checkdnsrr') && checkdnsrr($regs[1], 'MX')) {
+ return true;
+ }
+
+ return is_array(gethostbynamel($regs[1] . '.'));
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks that value is exactly $comparedTo.
+ *
+ * @param mixed $check Value to check
+ * @param mixed $comparedTo Value to compare
+ * @return bool Success
+ */
+ public static function equalTo($check, $comparedTo): bool
+ {
+ return $check === $comparedTo;
+ }
+
+ /**
+ * Checks that value has a valid file extension.
+ *
+ * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check
+ * @param string[] $extensions file extensions to allow. By default extensions are 'gif', 'jpeg', 'png', 'jpg'
+ * @return bool Success
+ */
+ public static function extension($check, array $extensions = ['gif', 'jpeg', 'png', 'jpg']): bool
+ {
+ if ($check instanceof UploadedFileInterface) {
+ $check = $check->getClientFilename();
+ } elseif (is_array($check) && isset($check['name'])) {
+ $check = $check['name'];
+ } elseif (is_array($check)) {
+ return static::extension(array_shift($check), $extensions);
+ }
+
+ if (empty($check)) {
+ return false;
+ }
+
+ $extension = strtolower(pathinfo($check, PATHINFO_EXTENSION));
+ foreach ($extensions as $value) {
+ if ($extension === strtolower($value)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Validation of an IP address.
+ *
+ * @param mixed $check The string to test.
+ * @param string $type The IP Protocol version to validate against
+ * @return bool Success
+ */
+ public static function ip($check, string $type = 'both'): bool
+ {
+ if (!is_string($check)) {
+ return false;
+ }
+
+ $type = strtolower($type);
+ $flags = 0;
+ if ($type === 'ipv4') {
+ $flags = FILTER_FLAG_IPV4;
+ }
+ if ($type === 'ipv6') {
+ $flags = FILTER_FLAG_IPV6;
+ }
+
+ return (bool)filter_var($check, FILTER_VALIDATE_IP, ['flags' => $flags]);
+ }
+
+ /**
+ * Checks whether the length of a string (in characters) is greater or equal to a minimal length.
+ *
+ * @param mixed $check The string to test
+ * @param int $min The minimal string length
+ * @return bool Success
+ */
+ public static function minLength($check, int $min): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+
+ return mb_strlen((string)$check) >= $min;
+ }
+
+ /**
+ * Checks whether the length of a string (in characters) is smaller or equal to a maximal length.
+ *
+ * @param mixed $check The string to test
+ * @param int $max The maximal string length
+ * @return bool Success
+ */
+ public static function maxLength($check, int $max): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+
+ return mb_strlen((string)$check) <= $max;
+ }
+
+ /**
+ * Checks whether the length of a string (in bytes) is greater or equal to a minimal length.
+ *
+ * @param mixed $check The string to test
+ * @param int $min The minimal string length (in bytes)
+ * @return bool Success
+ */
+ public static function minLengthBytes($check, int $min): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+
+ return strlen((string)$check) >= $min;
+ }
+
+ /**
+ * Checks whether the length of a string (in bytes) is smaller or equal to a maximal length.
+ *
+ * @param mixed $check The string to test
+ * @param int $max The maximal string length
+ * @return bool Success
+ */
+ public static function maxLengthBytes($check, int $max): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+
+ return strlen((string)$check) <= $max;
+ }
+
+ /**
+ * Checks that a value is a monetary amount.
+ *
+ * @param mixed $check Value to check
+ * @param string $symbolPosition Where symbol is located (left/right)
+ * @return bool Success
+ */
+ public static function money($check, string $symbolPosition = 'left'): bool
+ {
+ $money = '(?!0,?\d)(?:\d{1,3}(?:([, .])\d{3})?(?:\1\d{3})*|(?:\d+))((?!\1)[,.]\d{1,2})?';
+ if ($symbolPosition === 'right') {
+ $regex = '/^' . $money . '(? provide a list of choices that selections must be made from
+ * - max => maximum number of non-zero choices that can be made
+ * - min => minimum number of non-zero choices that can be made
+ *
+ * @param mixed $check Value to check
+ * @param array $options Options for the check.
+ * @param bool $caseInsensitive Set to true for case insensitive comparison.
+ * @return bool Success
+ */
+ public static function multiple($check, array $options = [], bool $caseInsensitive = false): bool
+ {
+ $defaults = ['in' => null, 'max' => null, 'min' => null];
+ $options += $defaults;
+
+ $check = array_filter((array)$check, function ($value) {
+ return $value || is_numeric($value);
+ });
+ if (empty($check)) {
+ return false;
+ }
+ if ($options['max'] && count($check) > $options['max']) {
+ return false;
+ }
+ if ($options['min'] && count($check) < $options['min']) {
+ return false;
+ }
+ if ($options['in'] && is_array($options['in'])) {
+ if ($caseInsensitive) {
+ $options['in'] = array_map('mb_strtolower', $options['in']);
+ }
+ foreach ($check as $val) {
+ $strict = !is_numeric($val);
+ if ($caseInsensitive) {
+ $val = mb_strtolower($val);
+ }
+ if (!in_array((string)$val, $options['in'], $strict)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if a value is numeric.
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function numeric($check): bool
+ {
+ return is_numeric($check);
+ }
+
+ /**
+ * Checks if a value is a natural number.
+ *
+ * @param mixed $check Value to check
+ * @param bool $allowZero Set true to allow zero, defaults to false
+ * @return bool Success
+ * @see https://en.wikipedia.org/wiki/Natural_number
+ */
+ public static function naturalNumber($check, bool $allowZero = false): bool
+ {
+ $regex = $allowZero ? '/^(?:0|[1-9][0-9]*)$/' : '/^[1-9][0-9]*$/';
+
+ return static::_check($check, $regex);
+ }
+
+ /**
+ * Validates that a number is in specified range.
+ *
+ * If $lower and $upper are set, the range is inclusive.
+ * If they are not set, will return true if $check is a
+ * legal finite on this platform.
+ *
+ * @param mixed $check Value to check
+ * @param float|null $lower Lower limit
+ * @param float|null $upper Upper limit
+ * @return bool Success
+ */
+ public static function range($check, ?float $lower = null, ?float $upper = null): bool
+ {
+ if (!is_numeric($check)) {
+ return false;
+ }
+ if ((float)$check != $check) {
+ return false;
+ }
+ if (isset($lower, $upper)) {
+ return $check >= $lower && $check <= $upper;
+ }
+
+ return is_finite((float)$check);
+ }
+
+ /**
+ * Checks that a value is a valid URL according to https://www.w3.org/Addressing/URL/url-spec.txt
+ *
+ * The regex checks for the following component parts:
+ *
+ * - a valid, optional, scheme
+ * - a valid IP address OR
+ * a valid domain name as defined by section 2.3.1 of https://www.ietf.org/rfc/rfc1035.txt
+ * with an optional port number
+ * - an optional valid path
+ * - an optional query string (get parameters)
+ * - an optional fragment (anchor tag) as defined in RFC 3986
+ *
+ * @param mixed $check Value to check
+ * @param bool $strict Require URL to be prefixed by a valid scheme (one of http(s)/ftp(s)/file/news/gopher)
+ * @return bool Success
+ * @link https://tools.ietf.org/html/rfc3986
+ */
+ public static function url($check, bool $strict = false): bool
+ {
+ if (!is_string($check)) {
+ return false;
+ }
+
+ static::_populateIp();
+
+ $emoji = '\x{1F190}-\x{1F9EF}';
+ $alpha = '0-9\p{L}\p{N}' . $emoji;
+ $hex = '(%[0-9a-f]{2})';
+ $subDelimiters = preg_quote('/!"$&\'()*+,-.@_:;=~[]', '/');
+ $path = '([' . $subDelimiters . $alpha . ']|' . $hex . ')';
+ $fragmentAndQuery = '([\?' . $subDelimiters . $alpha . ']|' . $hex . ')';
+ // phpcs:disable Generic.Files.LineLength
+ $regex = '/^(?:(?:https?|ftps?|sftp|file|news|gopher):\/\/)' . ($strict ? '' : '?') .
+ '(?:' . static::$_pattern['IPv4'] . '|\[' . static::$_pattern['IPv6'] . '\]|' . static::$_pattern['hostname'] . ')(?::[1-9][0-9]{0,4})?' .
+ '(?:\/' . $path . '*)?' .
+ '(?:\?' . $fragmentAndQuery . '*)?' .
+ '(?:#' . $fragmentAndQuery . '*)?$/iu';
+ // phpcs:enable Generic.Files.LineLength
+
+ return static::_check($check, $regex);
+ }
+
+ /**
+ * Checks if a value is in a given list. Comparison is case sensitive by default.
+ *
+ * @param mixed $check Value to check.
+ * @param string[] $list List to check against.
+ * @param bool $caseInsensitive Set to true for case insensitive comparison.
+ * @return bool Success.
+ */
+ public static function inList($check, array $list, bool $caseInsensitive = false): bool
+ {
+ if (!is_scalar($check)) {
+ return false;
+ }
+ if ($caseInsensitive) {
+ $list = array_map('mb_strtolower', $list);
+ $check = mb_strtolower((string)$check);
+ } else {
+ $list = array_map('strval', $list);
+ }
+
+ return in_array((string)$check, $list, true);
+ }
+
+ /**
+ * Checks that a value is a valid UUID - https://tools.ietf.org/html/rfc4122
+ *
+ * @param mixed $check Value to check
+ * @return bool Success
+ */
+ public static function uuid($check): bool
+ {
+ $regex = '/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[0-5][a-fA-F0-9]{3}-[089aAbB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$/';
+
+ return self::_check($check, $regex);
+ }
+
+ /**
+ * Runs a regular expression match.
+ *
+ * @param mixed $check Value to check against the $regex expression
+ * @param string $regex Regular expression
+ * @return bool Success of match
+ */
+ protected static function _check($check, string $regex): bool
+ {
+ return is_scalar($check) && preg_match($regex, (string)$check);
+ }
+
+ /**
+ * Luhn algorithm
+ *
+ * @param mixed $check Value to check.
+ * @return bool Success
+ * @see https://en.wikipedia.org/wiki/Luhn_algorithm
+ */
+ public static function luhn($check): bool
+ {
+ if (!is_scalar($check) || (int)$check === 0) {
+ return false;
+ }
+ $sum = 0;
+ $check = (string)$check;
+ $length = strlen($check);
+
+ for ($position = 1 - ($length % 2); $position < $length; $position += 2) {
+ $sum += (int)$check[$position];
+ }
+
+ for ($position = $length % 2; $position < $length; $position += 2) {
+ $number = (int)$check[$position] * 2;
+ $sum += $number < 10 ? $number : $number - 9;
+ }
+
+ return $sum % 10 === 0;
+ }
+
+ /**
+ * Checks the mime type of a file.
+ *
+ * Will check the mimetype of files/UploadedFileInterface instances
+ * by checking the using finfo on the file, not relying on the content-type
+ * sent by the client.
+ *
+ * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check.
+ * @param array|string $mimeTypes Array of mime types or regex pattern to check.
+ * @return bool Success
+ * @throws \RuntimeException when mime type can not be determined.
+ * @throws \LogicException when ext/fileinfo is missing
+ */
+ public static function mimeType($check, $mimeTypes = []): bool
+ {
+ $file = static::getFilename($check);
+ if ($file === false) {
+ return false;
+ }
+
+ if (!function_exists('finfo_open')) {
+ throw new LogicException('ext/fileinfo is required for validating file mime types');
+ }
+
+ if (!is_file($file)) {
+ throw new RuntimeException('Cannot validate mimetype for a missing file');
+ }
+
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $mime = finfo_file($finfo, $file);
+
+ if (!$mime) {
+ throw new RuntimeException('Can not determine the mimetype.');
+ }
+
+ if (is_string($mimeTypes)) {
+ return self::_check($mime, $mimeTypes);
+ }
+
+ foreach ($mimeTypes as $key => $val) {
+ $mimeTypes[$key] = strtolower($val);
+ }
+
+ return in_array(strtolower($mime), $mimeTypes, true);
+ }
+
+ /**
+ * Helper for reading the file out of the various file implementations
+ * we accept.
+ *
+ * @param mixed $check The data to read a filename out of.
+ * @return string|false Either the filename or false on failure.
+ */
+ protected static function getFilename($check)
+ {
+ if ($check instanceof UploadedFileInterface) {
+ // Uploaded files throw exceptions on upload errors.
+ try {
+ $uri = $check->getStream()->getMetadata('uri');
+ if (is_string($uri)) {
+ return $uri;
+ }
+
+ return false;
+ } catch (RuntimeException $e) {
+ return false;
+ }
+ }
+ if (is_array($check) && isset($check['tmp_name'])) {
+ return $check['tmp_name'];
+ }
+
+ if (is_string($check)) {
+ return $check;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the filesize
+ *
+ * Will check the filesize of files/UploadedFileInterface instances
+ * by checking the filesize() on disk and not relying on the length
+ * reported by the client.
+ *
+ * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check.
+ * @param string $operator See `Validation::comparison()`.
+ * @param int|string $size Size in bytes or human readable string like '5MB'.
+ * @return bool Success
+ */
+ public static function fileSize($check, string $operator, $size): bool
+ {
+ $file = static::getFilename($check);
+ if ($file === false) {
+ return false;
+ }
+
+ if (is_string($size)) {
+ $size = Text::parseFileSize($size);
+ }
+ $filesize = filesize($file);
+
+ return static::comparison($filesize, $operator, $size);
+ }
+
+ /**
+ * Checking for upload errors
+ *
+ * @param string|array|\Psr\Http\Message\UploadedFileInterface $check Value to check.
+ * @param bool $allowNoFile Set to true to allow UPLOAD_ERR_NO_FILE as a pass.
+ * @return bool
+ * @see https://secure.php.net/manual/en/features.file-upload.errors.php
+ */
+ public static function uploadError($check, bool $allowNoFile = false): bool
+ {
+ if ($check instanceof UploadedFileInterface) {
+ $code = $check->getError();
+ } elseif (is_array($check) && isset($check['error'])) {
+ $code = $check['error'];
+ } else {
+ $code = $check;
+ }
+ if ($allowNoFile) {
+ return in_array((int)$code, [UPLOAD_ERR_OK, UPLOAD_ERR_NO_FILE], true);
+ }
+
+ return (int)$code === UPLOAD_ERR_OK;
+ }
+
+ /**
+ * Validate an uploaded file.
+ *
+ * Helps join `uploadError`, `fileSize` and `mimeType` into
+ * one higher level validation method.
+ *
+ * ### Options
+ *
+ * - `types` - An array of valid mime types. If empty all types
+ * will be accepted. The `type` will not be looked at, instead
+ * the file type will be checked with ext/finfo.
+ * - `minSize` - The minimum file size in bytes. Defaults to not checking.
+ * - `maxSize` - The maximum file size in bytes. Defaults to not checking.
+ * - `optional` - Whether or not this file is optional. Defaults to false.
+ * If true a missing file will pass the validator regardless of other constraints.
+ *
+ * @param mixed $file The uploaded file data from PHP.
+ * @param array $options An array of options for the validation.
+ * @return bool
+ */
+ public static function uploadedFile($file, array $options = []): bool
+ {
+ $options += [
+ 'minSize' => null,
+ 'maxSize' => null,
+ 'types' => null,
+ 'optional' => false,
+ ];
+ if (!is_array($file) && !($file instanceof UploadedFileInterface)) {
+ return false;
+ }
+ $error = $isUploaded = false;
+ if ($file instanceof UploadedFileInterface) {
+ $error = $file->getError();
+ $isUploaded = true;
+ }
+ if (is_array($file)) {
+ $keys = ['error', 'name', 'size', 'tmp_name', 'type'];
+ ksort($file);
+ if (array_keys($file) !== $keys) {
+ return false;
+ }
+ $error = (int)$file['error'];
+ $isUploaded = is_uploaded_file($file['tmp_name']);
+ }
+
+ if (!static::uploadError($file, $options['optional'])) {
+ return false;
+ }
+ if ($options['optional'] && $error === UPLOAD_ERR_NO_FILE) {
+ return true;
+ }
+ if (
+ isset($options['minSize'])
+ && !static::fileSize($file, static::COMPARE_GREATER_OR_EQUAL, $options['minSize'])
+ ) {
+ return false;
+ }
+ if (
+ isset($options['maxSize'])
+ && !static::fileSize($file, static::COMPARE_LESS_OR_EQUAL, $options['maxSize'])
+ ) {
+ return false;
+ }
+ if (isset($options['types']) && !static::mimeType($file, $options['types'])) {
+ return false;
+ }
+
+ return $isUploaded;
+ }
+
+ /**
+ * Validates the size of an uploaded image.
+ *
+ * @param mixed $file The uploaded file data from PHP.
+ * @param array $options Options to validate width and height.
+ * @return bool
+ * @throws \InvalidArgumentException
+ */
+ public static function imageSize($file, array $options): bool
+ {
+ if (!isset($options['height']) && !isset($options['width'])) {
+ throw new InvalidArgumentException(
+ 'Invalid image size validation parameters! Missing `width` and / or `height`.'
+ );
+ }
+
+ $file = static::getFilename($file);
+ if ($file === false) {
+ return false;
+ }
+
+ [$width, $height] = getimagesize($file);
+ $validHeight = null;
+ $validWidth = null;
+
+ if (isset($options['height'])) {
+ $validHeight = self::comparison($height, $options['height'][0], $options['height'][1]);
+ }
+ if (isset($options['width'])) {
+ $validWidth = self::comparison($width, $options['width'][0], $options['width'][1]);
+ }
+ if ($validHeight !== null && $validWidth !== null) {
+ return $validHeight && $validWidth;
+ }
+ if ($validHeight !== null) {
+ return $validHeight;
+ }
+ if ($validWidth !== null) {
+ return $validWidth;
+ }
+
+ throw new InvalidArgumentException('The 2nd argument is missing the `width` and / or `height` options.');
+ }
+
+ /**
+ * Validates the image width.
+ *
+ * @param mixed $file The uploaded file data from PHP.
+ * @param string $operator Comparison operator.
+ * @param int $width Min or max width.
+ * @return bool
+ */
+ public static function imageWidth($file, string $operator, int $width): bool
+ {
+ return self::imageSize($file, [
+ 'width' => [
+ $operator,
+ $width,
+ ],
+ ]);
+ }
+
+ /**
+ * Validates the image height.
+ *
+ * @param mixed $file The uploaded file data from PHP.
+ * @param string $operator Comparison operator.
+ * @param int $height Min or max height.
+ * @return bool
+ */
+ public static function imageHeight($file, string $operator, int $height): bool
+ {
+ return self::imageSize($file, [
+ 'height' => [
+ $operator,
+ $height,
+ ],
+ ]);
+ }
+
+ /**
+ * Validates a geographic coordinate.
+ *
+ * Supported formats:
+ *
+ * - `, ` Example: `-25.274398, 133.775136`
+ *
+ * ### Options
+ *
+ * - `type` - A string of the coordinate format, right now only `latLong`.
+ * - `format` - By default `both`, can be `long` and `lat` as well to validate
+ * only a part of the coordinate.
+ *
+ * @param mixed $value Geographic location as string
+ * @param array $options Options for the validation logic.
+ * @return bool
+ */
+ public static function geoCoordinate($value, array $options = []): bool
+ {
+ if (!is_scalar($value)) {
+ return false;
+ }
+
+ $options += [
+ 'format' => 'both',
+ 'type' => 'latLong',
+ ];
+ if ($options['type'] !== 'latLong') {
+ throw new RuntimeException(sprintf(
+ 'Unsupported coordinate type "%s". Use "latLong" instead.',
+ $options['type']
+ ));
+ }
+ $pattern = '/^' . self::$_pattern['latitude'] . ',\s*' . self::$_pattern['longitude'] . '$/';
+ if ($options['format'] === 'long') {
+ $pattern = '/^' . self::$_pattern['longitude'] . '$/';
+ }
+ if ($options['format'] === 'lat') {
+ $pattern = '/^' . self::$_pattern['latitude'] . '$/';
+ }
+
+ return (bool)preg_match($pattern, (string)$value);
+ }
+
+ /**
+ * Convenience method for latitude validation.
+ *
+ * @param mixed $value Latitude as string
+ * @param array $options Options for the validation logic.
+ * @return bool
+ * @link https://en.wikipedia.org/wiki/Latitude
+ * @see \Cake\Validation\Validation::geoCoordinate()
+ */
+ public static function latitude($value, array $options = []): bool
+ {
+ $options['format'] = 'lat';
+
+ return self::geoCoordinate($value, $options);
+ }
+
+ /**
+ * Convenience method for longitude validation.
+ *
+ * @param mixed $value Latitude as string
+ * @param array $options Options for the validation logic.
+ * @return bool
+ * @link https://en.wikipedia.org/wiki/Longitude
+ * @see \Cake\Validation\Validation::geoCoordinate()
+ */
+ public static function longitude($value, array $options = []): bool
+ {
+ $options['format'] = 'long';
+
+ return self::geoCoordinate($value, $options);
+ }
+
+ /**
+ * Check that the input value is within the ascii byte range.
+ *
+ * This method will reject all non-string values.
+ *
+ * @param mixed $value The value to check
+ * @return bool
+ */
+ public static function ascii($value): bool
+ {
+ if (!is_string($value)) {
+ return false;
+ }
+
+ return strlen($value) <= mb_strlen($value, 'utf-8');
+ }
+
+ /**
+ * Check that the input value is a utf8 string.
+ *
+ * This method will reject all non-string values.
+ *
+ * # Options
+ *
+ * - `extended` - Disallow bytes higher within the basic multilingual plane.
+ * MySQL's older utf8 encoding type does not allow characters above
+ * the basic multilingual plane. Defaults to false.
+ *
+ * @param mixed $value The value to check
+ * @param array $options An array of options. See above for the supported options.
+ * @return bool
+ */
+ public static function utf8($value, array $options = []): bool
+ {
+ if (!is_string($value)) {
+ return false;
+ }
+ $options += ['extended' => false];
+ if ($options['extended']) {
+ return true;
+ }
+
+ return preg_match('/[\x{10000}-\x{10FFFF}]/u', $value) === 0;
+ }
+
+ /**
+ * Check that the input value is an integer
+ *
+ * This method will accept strings that contain only integer data
+ * as well.
+ *
+ * @param mixed $value The value to check
+ * @return bool
+ */
+ public static function isInteger($value): bool
+ {
+ if (is_int($value)) {
+ return true;
+ }
+
+ if (!is_string($value) || !is_numeric($value)) {
+ return false;
+ }
+
+ return (bool)preg_match('/^-?[0-9]+$/', $value);
+ }
+
+ /**
+ * Check that the input value is an array.
+ *
+ * @param mixed $value The value to check
+ * @return bool
+ */
+ public static function isArray($value): bool
+ {
+ return is_array($value);
+ }
+
+ /**
+ * Check that the input value is a scalar.
+ *
+ * This method will accept integers, floats, strings and booleans, but
+ * not accept arrays, objects, resources and nulls.
+ *
+ * @param mixed $value The value to check
+ * @return bool
+ */
+ public static function isScalar($value): bool
+ {
+ return is_scalar($value);
+ }
+
+ /**
+ * Check that the input value is a 6 digits hex color.
+ *
+ * @param mixed $check The value to check
+ * @return bool Success
+ */
+ public static function hexColor($check): bool
+ {
+ return static::_check($check, '/^#[0-9a-f]{6}$/iD');
+ }
+
+ /**
+ * Check that the input value has a valid International Bank Account Number IBAN syntax
+ * Requirements are uppercase, no whitespaces, max length 34, country code and checksum exist at right spots,
+ * body matches against checksum via Mod97-10 algorithm
+ *
+ * @param mixed $check The value to check
+ * @return bool Success
+ */
+ public static function iban($check): bool
+ {
+ if (
+ !is_string($check) ||
+ !preg_match('/^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$/', $check)
+ ) {
+ return false;
+ }
+
+ $country = substr($check, 0, 2);
+ $checkInt = intval(substr($check, 2, 2));
+ $account = substr($check, 4);
+ $search = range('A', 'Z');
+ $replace = [];
+ foreach (range(10, 35) as $tmp) {
+ $replace[] = strval($tmp);
+ }
+ $numStr = str_replace($search, $replace, $account . $country . '00');
+ $checksum = intval(substr($numStr, 0, 1));
+ $numStrLength = strlen($numStr);
+ for ($pos = 1; $pos < $numStrLength; $pos++) {
+ $checksum *= 10;
+ $checksum += intval(substr($numStr, $pos, 1));
+ $checksum %= 97;
+ }
+
+ return $checkInt === 98 - $checksum;
+ }
+
+ /**
+ * Converts an array representing a date or datetime into a ISO string.
+ * The arrays are typically sent for validation from a form generated by
+ * the CakePHP FormHelper.
+ *
+ * @param array $value The array representing a date or datetime.
+ * @return string
+ */
+ protected static function _getDateString(array $value): string
+ {
+ $formatted = '';
+ if (
+ isset($value['year'], $value['month'], $value['day']) &&
+ (
+ is_numeric($value['year']) &&
+ is_numeric($value['month']) &&
+ is_numeric($value['day'])
+ )
+ ) {
+ $formatted .= sprintf('%d-%02d-%02d ', $value['year'], $value['month'], $value['day']);
+ }
+
+ if (isset($value['hour'])) {
+ if (isset($value['meridian']) && (int)$value['hour'] === 12) {
+ $value['hour'] = 0;
+ }
+ if (isset($value['meridian'])) {
+ $value['hour'] = strtolower($value['meridian']) === 'am' ? $value['hour'] : $value['hour'] + 12;
+ }
+ $value += ['minute' => 0, 'second' => 0, 'microsecond' => 0];
+ if (
+ is_numeric($value['hour']) &&
+ is_numeric($value['minute']) &&
+ is_numeric($value['second']) &&
+ is_numeric($value['microsecond'])
+ ) {
+ $formatted .= sprintf(
+ '%02d:%02d:%02d.%06d',
+ $value['hour'],
+ $value['minute'],
+ $value['second'],
+ $value['microsecond']
+ );
+ }
+ }
+
+ return trim($formatted);
+ }
+
+ /**
+ * Lazily populate the IP address patterns used for validations
+ *
+ * @return void
+ */
+ protected static function _populateIp(): void
+ {
+ // phpcs:disable Generic.Files.LineLength
+ if (!isset(static::$_pattern['IPv6'])) {
+ $pattern = '((([0-9A-Fa-f]{1,4}:){7}(([0-9A-Fa-f]{1,4})|:))|(([0-9A-Fa-f]{1,4}:){6}';
+ $pattern .= '(:|((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})';
+ $pattern .= '|(:[0-9A-Fa-f]{1,4})))|(([0-9A-Fa-f]{1,4}:){5}((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})';
+ $pattern .= '(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:)';
+ $pattern .= '{4}(:[0-9A-Fa-f]{1,4}){0,1}((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2}))';
+ $pattern .= '{3})?)|((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:){3}(:[0-9A-Fa-f]{1,4}){0,2}';
+ $pattern .= '((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|';
+ $pattern .= '((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:){2}(:[0-9A-Fa-f]{1,4}){0,3}';
+ $pattern .= '((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2}))';
+ $pattern .= '{3})?)|((:[0-9A-Fa-f]{1,4}){1,2})))|(([0-9A-Fa-f]{1,4}:)(:[0-9A-Fa-f]{1,4})';
+ $pattern .= '{0,4}((:((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)';
+ $pattern .= '|((:[0-9A-Fa-f]{1,4}){1,2})))|(:(:[0-9A-Fa-f]{1,4}){0,5}((:((25[0-5]|2[0-4]';
+ $pattern .= '\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|((:[0-9A-Fa-f]{1,4})';
+ $pattern .= '{1,2})))|(((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})))(%.+)?';
+
+ static::$_pattern['IPv6'] = $pattern;
+ }
+ if (!isset(static::$_pattern['IPv4'])) {
+ $pattern = '(?:(?:25[0-5]|2[0-4][0-9]|(?:(?:1[0-9])?|[1-9]?)[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|(?:(?:1[0-9])?|[1-9]?)[0-9])';
+ static::$_pattern['IPv4'] = $pattern;
+ }
+ // phpcs:enable Generic.Files.LineLength
+ }
+
+ /**
+ * Reset internal variables for another validation run.
+ *
+ * @return void
+ */
+ protected static function _reset(): void
+ {
+ static::$errors = [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidationRule.php b/app/vendor/cakephp/cakephp/src/Validation/ValidationRule.php
new file mode 100644
index 000000000..087c0fd2d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/ValidationRule.php
@@ -0,0 +1,219 @@
+_addValidatorProps($validator);
+ }
+
+ /**
+ * Returns whether this rule should break validation process for associated field
+ * after it fails
+ *
+ * @return bool
+ */
+ public function isLast(): bool
+ {
+ return $this->_last;
+ }
+
+ /**
+ * Dispatches the validation rule to the given validator method and returns
+ * a boolean indicating whether the rule passed or not. If a string is returned
+ * it is assumed that the rule failed and the error message was given as a result.
+ *
+ * @param mixed $value The data to validate
+ * @param array $providers associative array with objects or class names that will
+ * be passed as the last argument for the validation method
+ * @param array $context A key value list of data that could be used as context
+ * during validation. Recognized keys are:
+ * - newRecord: (boolean) whether or not the data to be validated belongs to a
+ * new record
+ * - data: The full data that was passed to the validation process
+ * - field: The name of the field that is being processed
+ * @return bool|string|array
+ * @throws \InvalidArgumentException when the supplied rule is not a valid
+ * callable for the configured scope
+ */
+ public function process($value, array $providers, array $context = [])
+ {
+ $context += ['data' => [], 'newRecord' => true, 'providers' => $providers];
+
+ if ($this->_skip($context)) {
+ return true;
+ }
+
+ if (!is_string($this->_rule) && is_callable($this->_rule)) {
+ $callable = $this->_rule;
+ $isCallable = true;
+ } else {
+ $provider = $providers[$this->_provider];
+ $callable = [$provider, $this->_rule];
+ $isCallable = is_callable($callable);
+ }
+
+ if (!$isCallable) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $message = sprintf(
+ 'Unable to call method "%s" in "%s" provider for field "%s"',
+ $this->_rule,
+ $this->_provider,
+ $context['field']
+ );
+ throw new InvalidArgumentException($message);
+ }
+
+ if ($this->_pass) {
+ $args = array_values(array_merge([$value], $this->_pass, [$context]));
+ $result = $callable(...$args);
+ } else {
+ $result = $callable($value, $context);
+ }
+
+ if ($result === false) {
+ return $this->_message ?: false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks if the validation rule should be skipped
+ *
+ * @param array $context A key value list of data that could be used as context
+ * during validation. Recognized keys are:
+ * - newRecord: (boolean) whether or not the data to be validated belongs to a
+ * new record
+ * - data: The full data that was passed to the validation process
+ * - providers associative array with objects or class names that will
+ * be passed as the last argument for the validation method
+ * @return bool True if the ValidationRule should be skipped
+ */
+ protected function _skip(array $context): bool
+ {
+ if (!is_string($this->_on) && is_callable($this->_on)) {
+ $function = $this->_on;
+
+ return !$function($context);
+ }
+
+ $newRecord = $context['newRecord'];
+ if (!empty($this->_on)) {
+ return ($this->_on === Validator::WHEN_CREATE && !$newRecord)
+ || ($this->_on === Validator::WHEN_UPDATE && $newRecord);
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets the rule properties from the rule entry in validate
+ *
+ * @param array $validator [optional]
+ * @return void
+ */
+ protected function _addValidatorProps(array $validator = []): void
+ {
+ foreach ($validator as $key => $value) {
+ if (!isset($value) || empty($value)) {
+ continue;
+ }
+ if ($key === 'rule' && is_array($value) && !is_callable($value)) {
+ $this->_pass = array_slice($value, 1);
+ $value = array_shift($value);
+ }
+ if (in_array($key, ['rule', 'on', 'message', 'last', 'provider', 'pass'], true)) {
+ $this->{"_$key"} = $value;
+ }
+ }
+ }
+
+ /**
+ * Returns the value of a property by name
+ *
+ * @param string $property The name of the property to retrieve.
+ * @return mixed
+ */
+ public function get(string $property)
+ {
+ $property = '_' . $property;
+ if (isset($this->{$property})) {
+ return $this->{$property};
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidationSet.php b/app/vendor/cakephp/cakephp/src/Validation/ValidationSet.php
new file mode 100644
index 000000000..4ca0a3355
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/ValidationSet.php
@@ -0,0 +1,235 @@
+_validatePresent;
+ }
+
+ /**
+ * Sets whether a field is required to be present in data array.
+ *
+ * @param bool|string|callable $validatePresent Valid values are true, false, 'create', 'update' or a callable.
+ * @return $this
+ */
+ public function requirePresence($validatePresent)
+ {
+ $this->_validatePresent = $validatePresent;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether or not a field can be left empty.
+ *
+ * @return bool|string|callable
+ */
+ public function isEmptyAllowed()
+ {
+ return $this->_allowEmpty;
+ }
+
+ /**
+ * Sets whether a field value is allowed to be empty.
+ *
+ * @param bool|string|callable $allowEmpty Valid values are true, false,
+ * 'create', 'update' or a callable.
+ * @return $this
+ */
+ public function allowEmpty($allowEmpty)
+ {
+ $this->_allowEmpty = $allowEmpty;
+
+ return $this;
+ }
+
+ /**
+ * Gets a rule for a given name if exists
+ *
+ * @param string $name The name under which the rule is set.
+ * @return \Cake\Validation\ValidationRule|null
+ */
+ public function rule(string $name): ?ValidationRule
+ {
+ if (!empty($this->_rules[$name])) {
+ return $this->_rules[$name];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns all rules for this validation set
+ *
+ * @return \Cake\Validation\ValidationRule[]
+ */
+ public function rules(): array
+ {
+ return $this->_rules;
+ }
+
+ /**
+ * Sets a ValidationRule $rule with a $name
+ *
+ * ### Example:
+ *
+ * ```
+ * $set
+ * ->add('notBlank', ['rule' => 'notBlank'])
+ * ->add('inRange', ['rule' => ['between', 4, 10])
+ * ```
+ *
+ * @param string $name The name under which the rule should be set
+ * @param \Cake\Validation\ValidationRule|array $rule The validation rule to be set
+ * @return $this
+ */
+ public function add(string $name, $rule)
+ {
+ if (!($rule instanceof ValidationRule)) {
+ $rule = new ValidationRule($rule);
+ }
+ $this->_rules[$name] = $rule;
+
+ return $this;
+ }
+
+ /**
+ * Removes a validation rule from the set
+ *
+ * ### Example:
+ *
+ * ```
+ * $set
+ * ->remove('notBlank')
+ * ->remove('inRange')
+ * ```
+ *
+ * @param string $name The name under which the rule should be unset
+ * @return $this
+ */
+ public function remove(string $name)
+ {
+ unset($this->_rules[$name]);
+
+ return $this;
+ }
+
+ /**
+ * Returns whether an index exists in the rule set
+ *
+ * @param string $index name of the rule
+ * @return bool
+ */
+ public function offsetExists($index): bool
+ {
+ return isset($this->_rules[$index]);
+ }
+
+ /**
+ * Returns a rule object by its index
+ *
+ * @param string $index name of the rule
+ * @return \Cake\Validation\ValidationRule
+ */
+ public function offsetGet($index): ValidationRule
+ {
+ return $this->_rules[$index];
+ }
+
+ /**
+ * Sets or replace a validation rule
+ *
+ * @param string $index name of the rule
+ * @param \Cake\Validation\ValidationRule|array $rule Rule to add to $index
+ * @return void
+ */
+ public function offsetSet($index, $rule): void
+ {
+ $this->add($index, $rule);
+ }
+
+ /**
+ * Unsets a validation rule
+ *
+ * @param string $index name of the rule
+ * @return void
+ */
+ public function offsetUnset($index): void
+ {
+ unset($this->_rules[$index]);
+ }
+
+ /**
+ * Returns an iterator for each of the rules to be applied
+ *
+ * @return \Cake\Validation\ValidationRule[]
+ * @psalm-return \Traversable
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->_rules);
+ }
+
+ /**
+ * Returns the number of rules in this set
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->_rules);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/Validator.php b/app/vendor/cakephp/cakephp/src/Validation/Validator.php
new file mode 100644
index 000000000..c08021f5d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/Validator.php
@@ -0,0 +1,2750 @@
+
+ */
+ protected $_fields = [];
+
+ /**
+ * An associative array of objects or classes containing methods
+ * used for validation
+ *
+ * @var array
+ */
+ protected $_providers = [];
+
+ /**
+ * An associative array of objects or classes used as a default provider list
+ *
+ * @var array
+ */
+ protected static $_defaultProviders = [];
+
+ /**
+ * Contains the validation messages associated with checking the presence
+ * for each corresponding field.
+ *
+ * @var array
+ */
+ protected $_presenceMessages = [];
+
+ /**
+ * Whether or not to use I18n functions for translating default error messages
+ *
+ * @var bool
+ */
+ protected $_useI18n = false;
+
+ /**
+ * Contains the validation messages associated with checking the emptiness
+ * for each corresponding field.
+ *
+ * @var array
+ */
+ protected $_allowEmptyMessages = [];
+
+ /**
+ * Contains the flags which specify what is empty for each corresponding field.
+ *
+ * @var array
+ */
+ protected $_allowEmptyFlags = [];
+
+ /**
+ * Whether to apply last flag to generated rule(s).
+ *
+ * @var bool
+ */
+ protected $_stopOnFailure = false;
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $this->_useI18n = function_exists('__d');
+ $this->_providers = self::$_defaultProviders;
+ }
+
+ /**
+ * Whether to stop validation rule evaluation on the first failed rule.
+ *
+ * When enabled the first failing rule per field will cause validation to stop.
+ * When disabled all rules will be run even if there are failures.
+ *
+ * @param bool $stopOnFailure If to apply last flag.
+ * @return $this
+ */
+ public function setStopOnFailure(bool $stopOnFailure = true)
+ {
+ $this->_stopOnFailure = $stopOnFailure;
+
+ return $this;
+ }
+
+ /**
+ * Validates and returns an array of failed fields and their error messages.
+ *
+ * @param array $data The data to be checked for errors
+ * @param bool $newRecord whether the data to be validated is new or to be updated.
+ * @return array[] Array of failed fields
+ * @deprecated 3.9.0 Renamed to {@link validate()}.
+ */
+ public function errors(array $data, bool $newRecord = true): array
+ {
+ deprecationWarning('`Validator::errors()` is deprecated. Use `Validator::validate()` instead.');
+
+ return $this->validate($data, $newRecord);
+ }
+
+ /**
+ * Validates and returns an array of failed fields and their error messages.
+ *
+ * @param array $data The data to be checked for errors
+ * @param bool $newRecord whether the data to be validated is new or to be updated.
+ * @return array[] Array of failed fields
+ */
+ public function validate(array $data, bool $newRecord = true): array
+ {
+ $errors = [];
+
+ foreach ($this->_fields as $name => $field) {
+ $keyPresent = array_key_exists($name, $data);
+
+ $providers = $this->_providers;
+ $context = compact('data', 'newRecord', 'field', 'providers');
+
+ if (!$keyPresent && !$this->_checkPresence($field, $context)) {
+ $errors[$name]['_required'] = $this->getRequiredMessage($name);
+ continue;
+ }
+ if (!$keyPresent) {
+ continue;
+ }
+
+ $canBeEmpty = $this->_canBeEmpty($field, $context);
+
+ $flags = static::EMPTY_NULL;
+ if (isset($this->_allowEmptyFlags[$name])) {
+ $flags = $this->_allowEmptyFlags[$name];
+ }
+
+ $isEmpty = $this->isEmpty($data[$name], $flags);
+
+ if (!$canBeEmpty && $isEmpty) {
+ $errors[$name]['_empty'] = $this->getNotEmptyMessage($name);
+ continue;
+ }
+
+ if ($isEmpty) {
+ continue;
+ }
+
+ $result = $this->_processRules($name, $field, $data, $newRecord);
+ if ($result) {
+ $errors[$name] = $result;
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Returns a ValidationSet object containing all validation rules for a field, if
+ * passed a ValidationSet as second argument, it will replace any other rule set defined
+ * before
+ *
+ * @param string $name [optional] The fieldname to fetch.
+ * @param \Cake\Validation\ValidationSet|null $set The set of rules for field
+ * @return \Cake\Validation\ValidationSet
+ */
+ public function field(string $name, ?ValidationSet $set = null): ValidationSet
+ {
+ if (empty($this->_fields[$name])) {
+ $set = $set ?: new ValidationSet();
+ $this->_fields[$name] = $set;
+ }
+
+ return $this->_fields[$name];
+ }
+
+ /**
+ * Check whether or not a validator contains any rules for the given field.
+ *
+ * @param string $name The field name to check.
+ * @return bool
+ */
+ public function hasField(string $name): bool
+ {
+ return isset($this->_fields[$name]);
+ }
+
+ /**
+ * Associates an object to a name so it can be used as a provider. Providers are
+ * objects or class names that can contain methods used during validation of for
+ * deciding whether a validation rule can be applied. All validation methods,
+ * when called will receive the full list of providers stored in this validator.
+ *
+ * @param string $name The name under which the provider should be set.
+ * @param object|string $object Provider object or class name.
+ * @return $this
+ */
+ public function setProvider(string $name, $object)
+ {
+ $this->_providers[$name] = $object;
+
+ return $this;
+ }
+
+ /**
+ * Returns the provider stored under that name if it exists.
+ *
+ * @param string $name The name under which the provider should be set.
+ * @return object|string|null
+ */
+ public function getProvider(string $name)
+ {
+ if (isset($this->_providers[$name])) {
+ return $this->_providers[$name];
+ }
+ if ($name !== 'default') {
+ return null;
+ }
+
+ $this->_providers[$name] = new RulesProvider();
+
+ return $this->_providers[$name];
+ }
+
+ /**
+ * Returns the default provider stored under that name if it exists.
+ *
+ * @param string $name The name under which the provider should be retrieved.
+ * @return object|string|null
+ */
+ public static function getDefaultProvider(string $name)
+ {
+ if (!isset(self::$_defaultProviders[$name])) {
+ return null;
+ }
+
+ return self::$_defaultProviders[$name];
+ }
+
+ /**
+ * Associates an object to a name so it can be used as a default provider.
+ *
+ * @param string $name The name under which the provider should be set.
+ * @param object|string $object Provider object or class name.
+ * @return void
+ */
+ public static function addDefaultProvider(string $name, $object): void
+ {
+ self::$_defaultProviders[$name] = $object;
+ }
+
+ /**
+ * Get the list of default providers.
+ *
+ * @return string[]
+ */
+ public static function getDefaultProviders(): array
+ {
+ return array_keys(self::$_defaultProviders);
+ }
+
+ /**
+ * Get the list of providers in this validator.
+ *
+ * @return string[]
+ */
+ public function providers(): array
+ {
+ return array_keys($this->_providers);
+ }
+
+ /**
+ * Returns whether a rule set is defined for a field or not
+ *
+ * @param string $field name of the field to check
+ * @return bool
+ */
+ public function offsetExists($field): bool
+ {
+ return isset($this->_fields[$field]);
+ }
+
+ /**
+ * Returns the rule set for a field
+ *
+ * @param string $field name of the field to check
+ * @return \Cake\Validation\ValidationSet
+ */
+ public function offsetGet($field): ValidationSet
+ {
+ return $this->field($field);
+ }
+
+ /**
+ * Sets the rule set for a field
+ *
+ * @param string $field name of the field to set
+ * @param array|\Cake\Validation\ValidationSet $rules set of rules to apply to field
+ * @return void
+ */
+ public function offsetSet($field, $rules): void
+ {
+ if (!$rules instanceof ValidationSet) {
+ $set = new ValidationSet();
+ foreach ($rules as $name => $rule) {
+ $set->add($name, $rule);
+ }
+ $rules = $set;
+ }
+ $this->_fields[$field] = $rules;
+ }
+
+ /**
+ * Unsets the rule set for a field
+ *
+ * @param string $field name of the field to unset
+ * @return void
+ */
+ public function offsetUnset($field): void
+ {
+ unset($this->_fields[$field]);
+ }
+
+ /**
+ * Returns an iterator for each of the fields to be validated
+ *
+ * @return \Cake\Validation\ValidationSet[]
+ * @psalm-return \Traversable
+ */
+ public function getIterator(): Traversable
+ {
+ return new ArrayIterator($this->_fields);
+ }
+
+ /**
+ * Returns the number of fields having validation rules
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->_fields);
+ }
+
+ /**
+ * Adds a new rule to a field's rule set. If second argument is an array
+ * then rules list for the field will be replaced with second argument and
+ * third argument will be ignored.
+ *
+ * ### Example:
+ *
+ * ```
+ * $validator
+ * ->add('title', 'required', ['rule' => 'notBlank'])
+ * ->add('user_id', 'valid', ['rule' => 'numeric', 'message' => 'Invalid User'])
+ *
+ * $validator->add('password', [
+ * 'size' => ['rule' => ['lengthBetween', 8, 20]],
+ * 'hasSpecialCharacter' => ['rule' => 'validateSpecialchar', 'message' => 'not valid']
+ * ]);
+ * ```
+ *
+ * @param string $field The name of the field from which the rule will be added
+ * @param array|string $name The alias for a single rule or multiple rules array
+ * @param array|\Cake\Validation\ValidationRule $rule the rule to add
+ * @throws \InvalidArgumentException If numeric index cannot be resolved to a string one
+ * @return $this
+ */
+ public function add(string $field, $name, $rule = [])
+ {
+ $validationSet = $this->field($field);
+
+ if (!is_array($name)) {
+ $rules = [$name => $rule];
+ } else {
+ $rules = $name;
+ }
+
+ foreach ($rules as $name => $rule) {
+ if (is_array($rule)) {
+ $rule += [
+ 'rule' => $name,
+ 'last' => $this->_stopOnFailure,
+ ];
+ }
+ if (!is_string($name)) {
+ /** @psalm-suppress PossiblyUndefinedMethod */
+ $name = $rule['rule'];
+ if (is_array($name)) {
+ $name = array_shift($name);
+ }
+
+ if ($validationSet->offsetExists($name)) {
+ $message = 'You cannot add a rule without a unique name, already existing rule found: ' . $name;
+ throw new InvalidArgumentException($message);
+ }
+
+ deprecationWarning(
+ 'Adding validation rules without a name key is deprecated. Update rules array to have string keys.'
+ );
+ }
+
+ $validationSet->add($name, $rule);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Adds a nested validator.
+ *
+ * Nesting validators allows you to define validators for array
+ * types. For example, nested validators are ideal when you want to validate a
+ * sub-document, or complex array type.
+ *
+ * This method assumes that the sub-document has a 1:1 relationship with the parent.
+ *
+ * The providers of the parent validator will be synced into the nested validator, when
+ * errors are checked. This ensures that any validation rule providers connected
+ * in the parent will have the same values in the nested validator when rules are evaluated.
+ *
+ * @param string $field The root field for the nested validator.
+ * @param \Cake\Validation\Validator $validator The nested validator.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @return $this
+ */
+ public function addNested(string $field, Validator $validator, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['message' => $message, 'on' => $when]);
+
+ $validationSet = $this->field($field);
+ $validationSet->add(static::NESTED, $extra + ['rule' => function ($value, $context) use ($validator, $message) {
+ if (!is_array($value)) {
+ return false;
+ }
+ foreach ($this->providers() as $provider) {
+ /** @psalm-suppress PossiblyNullArgument */
+ $validator->setProvider($provider, $this->getProvider($provider));
+ }
+ $errors = $validator->validate($value, $context['newRecord']);
+
+ $message = $message ? [static::NESTED => $message] : [];
+
+ return empty($errors) ? true : $errors + $message;
+ }]);
+
+ return $this;
+ }
+
+ /**
+ * Adds a nested validator.
+ *
+ * Nesting validators allows you to define validators for array
+ * types. For example, nested validators are ideal when you want to validate many
+ * similar sub-documents or complex array types.
+ *
+ * This method assumes that the sub-document has a 1:N relationship with the parent.
+ *
+ * The providers of the parent validator will be synced into the nested validator, when
+ * errors are checked. This ensures that any validation rule providers connected
+ * in the parent will have the same values in the nested validator when rules are evaluated.
+ *
+ * @param string $field The root field for the nested validator.
+ * @param \Cake\Validation\Validator $validator The nested validator.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @return $this
+ */
+ public function addNestedMany(string $field, Validator $validator, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['message' => $message, 'on' => $when]);
+
+ $validationSet = $this->field($field);
+ $validationSet->add(static::NESTED, $extra + ['rule' => function ($value, $context) use ($validator, $message) {
+ if (!is_array($value)) {
+ return false;
+ }
+ foreach ($this->providers() as $provider) {
+ /** @psalm-suppress PossiblyNullArgument */
+ $validator->setProvider($provider, $this->getProvider($provider));
+ }
+ $errors = [];
+ foreach ($value as $i => $row) {
+ if (!is_array($row)) {
+ return false;
+ }
+ $check = $validator->validate($row, $context['newRecord']);
+ if (!empty($check)) {
+ $errors[$i] = $check;
+ }
+ }
+
+ $message = $message ? [static::NESTED => $message] : [];
+
+ return empty($errors) ? true : $errors + $message;
+ }]);
+
+ return $this;
+ }
+
+ /**
+ * Removes a rule from the set by its name
+ *
+ * ### Example:
+ *
+ * ```
+ * $validator
+ * ->remove('title', 'required')
+ * ->remove('user_id')
+ * ```
+ *
+ * @param string $field The name of the field from which the rule will be removed
+ * @param string|null $rule the name of the rule to be removed
+ * @return $this
+ */
+ public function remove(string $field, ?string $rule = null)
+ {
+ if ($rule === null) {
+ unset($this->_fields[$field]);
+ } else {
+ $this->field($field)->remove($rule);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Sets whether a field is required to be present in data array.
+ * You can also pass array. Using an array will let you provide the following
+ * keys:
+ *
+ * - `mode` individual mode for field
+ * - `message` individual error message for field
+ *
+ * You can also set mode and message for all passed fields, the individual
+ * setting takes precedence over group settings.
+ *
+ * @param string|array $field the name of the field or list of fields.
+ * @param bool|string|callable $mode Valid values are true, false, 'create', 'update'.
+ * If a callable is passed then the field will be required only when the callback
+ * returns true.
+ * @param string|null $message The message to show if the field presence validation fails.
+ * @return $this
+ */
+ public function requirePresence($field, $mode = true, ?string $message = null)
+ {
+ $defaults = [
+ 'mode' => $mode,
+ 'message' => $message,
+ ];
+
+ if (!is_array($field)) {
+ $field = $this->_convertValidatorToArray($field, $defaults);
+ }
+
+ foreach ($field as $fieldName => $setting) {
+ $settings = $this->_convertValidatorToArray($fieldName, $defaults, $setting);
+ $fieldName = current(array_keys($settings));
+
+ $this->field($fieldName)->requirePresence($settings[$fieldName]['mode']);
+ if ($settings[$fieldName]['message']) {
+ $this->_presenceMessages[$fieldName] = $settings[$fieldName]['message'];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows a field to be empty. You can also pass array.
+ * Using an array will let you provide the following keys:
+ *
+ * - `when` individual when condition for field
+ * - 'message' individual message for field
+ *
+ * You can also set when and message for all passed fields, the individual setting
+ * takes precedence over group settings.
+ *
+ * This is the opposite of notEmpty() which requires a field to not be empty.
+ * By using $mode equal to 'create' or 'update', you can allow fields to be empty
+ * when records are first created, or when they are updated.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Email can be empty
+ * $validator->allowEmpty('email');
+ *
+ * // Email can be empty on create
+ * $validator->allowEmpty('email', Validator::WHEN_CREATE);
+ *
+ * // Email can be empty on update
+ * $validator->allowEmpty('email', Validator::WHEN_UPDATE);
+ *
+ * // Email and subject can be empty on update
+ * $validator->allowEmpty(['email', 'subject'], Validator::WHEN_UPDATE;
+ *
+ * // Email can be always empty, subject and content can be empty on update.
+ * $validator->allowEmpty(
+ * [
+ * 'email' => [
+ * 'when' => true
+ * ],
+ * 'content' => [
+ * 'message' => 'Content cannot be empty'
+ * ],
+ * 'subject'
+ * ],
+ * Validator::WHEN_UPDATE
+ * );
+ * ```
+ *
+ * It is possible to conditionally allow emptiness on a field by passing a callback
+ * as a second argument. The callback will receive the validation context array as
+ * argument:
+ *
+ * ```
+ * $validator->allowEmpty('email', function ($context) {
+ * return !$context['newRecord'] || $context['data']['role'] === 'admin';
+ * });
+ * ```
+ *
+ * This method will correctly detect empty file uploads and date/time/datetime fields.
+ *
+ * Because this and `notEmpty()` modify the same internal state, the last
+ * method called will take precedence.
+ *
+ * @deprecated 3.7.0 Use {@link allowEmptyString()}, {@link allowEmptyArray()}, {@link allowEmptyFile()},
+ * {@link allowEmptyDate()}, {@link allowEmptyTime()}, {@link allowEmptyDateTime()} or {@link allowEmptyFor()} instead.
+ * @param string|array $field the name of the field or a list of fields
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true (always), 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @param string|null $message The message to show if the field is not
+ * @return $this
+ */
+ public function allowEmpty($field, $when = true, $message = null)
+ {
+ deprecationWarning(
+ 'allowEmpty() is deprecated. '
+ . 'Use allowEmptyString(), allowEmptyArray(), allowEmptyFile(), allowEmptyDate(), allowEmptyTime(), '
+ . 'allowEmptyDateTime() or allowEmptyFor() instead.'
+ );
+
+ $defaults = [
+ 'when' => $when,
+ 'message' => $message,
+ ];
+ if (!is_array($field)) {
+ $field = $this->_convertValidatorToArray($field, $defaults);
+ }
+
+ foreach ($field as $fieldName => $setting) {
+ $settings = $this->_convertValidatorToArray($fieldName, $defaults, $setting);
+ $fieldName = array_keys($settings)[0];
+ $this->allowEmptyFor(
+ $fieldName,
+ static::EMPTY_ALL,
+ $settings[$fieldName]['when'],
+ $settings[$fieldName]['message']
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Low-level method to indicate that a field can be empty.
+ *
+ * This method should generally not be used and instead you should
+ * use:
+ *
+ * - `allowEmptyString()`
+ * - `allowEmptyArray()`
+ * - `allowEmptyFile()`
+ * - `allowEmptyDate()`
+ * - `allowEmptyDatetime()`
+ * - `allowEmptyTime()`
+ *
+ * Should be used as their APIs are simpler to operate and read.
+ *
+ * You can also set flags, when and message for all passed fields, the individual
+ * setting takes precedence over group settings.
+ *
+ * ### Example:
+ *
+ * ```
+ * // Email can be empty
+ * $validator->allowEmptyFor('email', Validator::EMPTY_STRING);
+ *
+ * // Email can be empty on create
+ * $validator->allowEmptyFor('email', Validator::EMPTY_STRING, Validator::WHEN_CREATE);
+ *
+ * // Email can be empty on update
+ * $validator->allowEmptyFor('email', Validator::EMPTY_STRING, Validator::WHEN_UPDATE);
+ * ```
+ *
+ * It is possible to conditionally allow emptiness on a field by passing a callback
+ * as a second argument. The callback will receive the validation context array as
+ * argument:
+ *
+ * ```
+ * $validator->allowEmpty('email', Validator::EMPTY_STRING, function ($context) {
+ * return !$context['newRecord'] || $context['data']['role'] === 'admin';
+ * });
+ * ```
+ *
+ * If you want to allow other kind of empty data on a field, you need to pass other
+ * flags:
+ *
+ * ```
+ * $validator->allowEmptyFor('photo', Validator::EMPTY_FILE);
+ * $validator->allowEmptyFor('published', Validator::EMPTY_STRING | Validator::EMPTY_DATE | Validator::EMPTY_TIME);
+ * $validator->allowEmptyFor('items', Validator::EMPTY_STRING | Validator::EMPTY_ARRAY);
+ * ```
+ *
+ * You can also use convenience wrappers of this method. The following calls are the
+ * same as above:
+ *
+ * ```
+ * $validator->allowEmptyFile('photo');
+ * $validator->allowEmptyDateTime('published');
+ * $validator->allowEmptyArray('items');
+ * ```
+ *
+ * @param string $field The name of the field.
+ * @param int|null $flags A bitmask of EMPTY_* flags which specify what is empty.
+ * If no flags/bitmask is provided only `null` will be allowed as empty value.
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, false, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @param string|null $message The message to show if the field is not
+ * @since 3.7.0
+ * @return $this
+ */
+ public function allowEmptyFor(string $field, ?int $flags = null, $when = true, ?string $message = null)
+ {
+ $this->field($field)->allowEmpty($when);
+ if ($message) {
+ $this->_allowEmptyMessages[$field] = $message;
+ }
+ if ($flags !== null) {
+ $this->_allowEmptyFlags[$field] = $flags;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows a field to be an empty string.
+ *
+ * This method is equivalent to calling allowEmptyFor() with EMPTY_STRING flag.
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, false, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @return $this
+ * @see \Cake\Validation\Validator::allowEmptyFor() For detail usage
+ */
+ public function allowEmptyString(string $field, ?string $message = null, $when = true)
+ {
+ return $this->allowEmptyFor($field, self::EMPTY_STRING, $when, $message);
+ }
+
+ /**
+ * Requires a field to be not be an empty string.
+ *
+ * Opposite to allowEmptyString()
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is empty.
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are false (never), 'create', 'update'. If a
+ * callable is passed then the field will be required to be not empty when
+ * the callback returns true.
+ * @return $this
+ * @see \Cake\Validation\Validator::allowEmptyString()
+ * @since 3.8.0
+ */
+ public function notEmptyString(string $field, ?string $message = null, $when = false)
+ {
+ $when = $this->invertWhenClause($when);
+
+ return $this->allowEmptyFor($field, self::EMPTY_STRING, $when, $message);
+ }
+
+ /**
+ * Allows a field to be an empty array.
+ *
+ * This method is equivalent to calling allowEmptyFor() with EMPTY_STRING +
+ * EMPTY_ARRAY flags.
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, false, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @return $this
+ * @since 3.7.0
+ * @see \Cake\Validation\Validator::allowEmptyFor() for examples.
+ */
+ public function allowEmptyArray(string $field, ?string $message = null, $when = true)
+ {
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_ARRAY, $when, $message);
+ }
+
+ /**
+ * Require a field to be a non-empty array
+ *
+ * Opposite to allowEmptyArray()
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is empty.
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are false (never), 'create', 'update'. If a
+ * callable is passed then the field will be required to be not empty when
+ * the callback returns true.
+ * @return $this
+ * @see \Cake\Validation\Validator::allowEmptyArray()
+ */
+ public function notEmptyArray(string $field, ?string $message = null, $when = false)
+ {
+ $when = $this->invertWhenClause($when);
+
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_ARRAY, $when, $message);
+ }
+
+ /**
+ * Allows a field to be an empty file.
+ *
+ * This method is equivalent to calling allowEmptyFor() with EMPTY_FILE flag.
+ * File fields will not accept `''`, or `[]` as empty values. Only `null` and a file
+ * upload with `error` equal to `UPLOAD_ERR_NO_FILE` will be treated as empty.
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @return $this
+ * @since 3.7.0
+ * @see \Cake\Validation\Validator::allowEmptyFor() For detail usage
+ */
+ public function allowEmptyFile(string $field, ?string $message = null, $when = true)
+ {
+ return $this->allowEmptyFor($field, self::EMPTY_FILE, $when, $message);
+ }
+
+ /**
+ * Require a field to be a not-empty file.
+ *
+ * Opposite to allowEmptyFile()
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is empty.
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are false (never), 'create', 'update'. If a
+ * callable is passed then the field will be required to be not empty when
+ * the callback returns true.
+ * @return $this
+ * @since 3.8.0
+ * @see \Cake\Validation\Validator::allowEmptyFile()
+ */
+ public function notEmptyFile(string $field, ?string $message = null, $when = false)
+ {
+ $when = $this->invertWhenClause($when);
+
+ return $this->allowEmptyFor($field, self::EMPTY_FILE, $when, $message);
+ }
+
+ /**
+ * Allows a field to be an empty date.
+ *
+ * Empty date values are `null`, `''`, `[]` and arrays where all values are `''`
+ * and the `year` key is present.
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, false, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @return $this
+ * @see \Cake\Validation\Validator::allowEmptyFor() for examples
+ */
+ public function allowEmptyDate(string $field, ?string $message = null, $when = true)
+ {
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_DATE, $when, $message);
+ }
+
+ /**
+ * Require a non-empty date value
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is empty.
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are false (never), 'create', 'update'. If a
+ * callable is passed then the field will be required to be not empty when
+ * the callback returns true.
+ * @return $this
+ * @see \Cake\Validation\Validator::allowEmptyDate() for examples
+ */
+ public function notEmptyDate(string $field, ?string $message = null, $when = false)
+ {
+ $when = $this->invertWhenClause($when);
+
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_DATE, $when, $message);
+ }
+
+ /**
+ * Allows a field to be an empty time.
+ *
+ * Empty date values are `null`, `''`, `[]` and arrays where all values are `''`
+ * and the `hour` key is present.
+ *
+ * This method is equivalent to calling allowEmptyFor() with EMPTY_STRING +
+ * EMPTY_TIME flags.
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, false, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns true.
+ * @return $this
+ * @since 3.7.0
+ * @see \Cake\Validation\Validator::allowEmptyFor() for examples.
+ */
+ public function allowEmptyTime(string $field, ?string $message = null, $when = true)
+ {
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_TIME, $when, $message);
+ }
+
+ /**
+ * Require a field to be a non-empty time.
+ *
+ * Opposite to allowEmptyTime()
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is empty.
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are false (never), 'create', 'update'. If a
+ * callable is passed then the field will be required to be not empty when
+ * the callback returns true.
+ * @return $this
+ * @since 3.8.0
+ * @see \Cake\Validation\Validator::allowEmptyTime()
+ */
+ public function notEmptyTime(string $field, ?string $message = null, $when = false)
+ {
+ $when = $this->invertWhenClause($when);
+
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_TIME, $when, $message);
+ }
+
+ /**
+ * Allows a field to be an empty date/time.
+ *
+ * Empty date values are `null`, `''`, `[]` and arrays where all values are `''`
+ * and the `year` and `hour` keys are present.
+ *
+ * This method is equivalent to calling allowEmptyFor() with EMPTY_STRING +
+ * EMPTY_DATE + EMPTY_TIME flags.
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is allowed to be empty
+ * Valid values are true, false, 'create', 'update'. If a callable is passed then
+ * the field will allowed to be empty only when the callback returns false.
+ * @return $this
+ * @since 3.7.0
+ * @see \Cake\Validation\Validator::allowEmptyFor() for examples.
+ */
+ public function allowEmptyDateTime(string $field, ?string $message = null, $when = true)
+ {
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_DATE | self::EMPTY_TIME, $when, $message);
+ }
+
+ /**
+ * Require a field to be a non empty date/time.
+ *
+ * Opposite to allowEmptyDateTime
+ *
+ * @param string $field The name of the field.
+ * @param string|null $message The message to show if the field is empty.
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are false (never), 'create', 'update'. If a
+ * callable is passed then the field will be required to be not empty when
+ * the callback returns true.
+ * @return $this
+ * @since 3.8.0
+ * @see \Cake\Validation\Validator::allowEmptyDateTime()
+ */
+ public function notEmptyDateTime(string $field, ?string $message = null, $when = false)
+ {
+ $when = $this->invertWhenClause($when);
+
+ return $this->allowEmptyFor($field, self::EMPTY_STRING | self::EMPTY_DATE | self::EMPTY_TIME, $when, $message);
+ }
+
+ /**
+ * Converts validator to fieldName => $settings array
+ *
+ * @param int|string $fieldName name of field
+ * @param array $defaults default settings
+ * @param string|array $settings settings from data
+ * @return array[]
+ * @throws \InvalidArgumentException
+ */
+ protected function _convertValidatorToArray($fieldName, array $defaults = [], $settings = []): array
+ {
+ if (is_string($settings)) {
+ $fieldName = $settings;
+ $settings = [];
+ }
+ if (!is_array($settings)) {
+ throw new InvalidArgumentException(
+ sprintf('Invalid settings for "%s". Settings must be an array.', $fieldName)
+ );
+ }
+ $settings += $defaults;
+
+ return [$fieldName => $settings];
+ }
+
+ /**
+ * Sets a field to require a non-empty value. You can also pass array.
+ * Using an array will let you provide the following keys:
+ *
+ * - `when` individual when condition for field
+ * - `message` individual error message for field
+ *
+ * You can also set `when` and `message` for all passed fields, the individual setting
+ * takes precedence over group settings.
+ *
+ * This is the opposite of `allowEmpty()` which allows a field to be empty.
+ * By using $mode equal to 'create' or 'update', you can make fields required
+ * when records are first created, or when they are updated.
+ *
+ * ### Example:
+ *
+ * ```
+ * $message = 'This field cannot be empty';
+ *
+ * // Email cannot be empty
+ * $validator->notEmpty('email');
+ *
+ * // Email can be empty on update, but not create
+ * $validator->notEmpty('email', $message, 'create');
+ *
+ * // Email can be empty on create, but required on update.
+ * $validator->notEmpty('email', $message, Validator::WHEN_UPDATE);
+ *
+ * // Email and title can be empty on create, but are required on update.
+ * $validator->notEmpty(['email', 'title'], $message, Validator::WHEN_UPDATE);
+ *
+ * // Email can be empty on create, title must always be not empty
+ * $validator->notEmpty(
+ * [
+ * 'email',
+ * 'title' => [
+ * 'when' => true,
+ * 'message' => 'Title cannot be empty'
+ * ]
+ * ],
+ * $message,
+ * Validator::WHEN_UPDATE
+ * );
+ * ```
+ *
+ * It is possible to conditionally disallow emptiness on a field by passing a callback
+ * as the third argument. The callback will receive the validation context array as
+ * argument:
+ *
+ * ```
+ * $validator->notEmpty('email', 'Email is required', function ($context) {
+ * return $context['newRecord'] && $context['data']['role'] !== 'admin';
+ * });
+ * ```
+ *
+ * Because this and `allowEmpty()` modify the same internal state, the last
+ * method called will take precedence.
+ *
+ * @deprecated 3.7.0 Use {@link notEmptyString()}, {@link notEmptyArray()}, {@link notEmptyFile()},
+ * {@link notEmptyDate()}, {@link notEmptyTime()} or {@link notEmptyDateTime()} instead.
+ * @param string|array $field the name of the field or list of fields
+ * @param string|null $message The message to show if the field is not
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are true (always), 'create', 'update'. If a
+ * callable is passed then the field will allowed to be empty only when
+ * the callback returns false.
+ * @return $this
+ */
+ public function notEmpty($field, ?string $message = null, $when = false)
+ {
+ deprecationWarning(
+ 'notEmpty() is deprecated. '
+ . 'Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() '
+ . 'or notEmptyDateTime() instead.'
+ );
+
+ $defaults = [
+ 'when' => $when,
+ 'message' => $message,
+ ];
+
+ if (!is_array($field)) {
+ $field = $this->_convertValidatorToArray($field, $defaults);
+ }
+
+ foreach ($field as $fieldName => $setting) {
+ $settings = $this->_convertValidatorToArray($fieldName, $defaults, $setting);
+ $fieldName = current(array_keys($settings));
+
+ $whenSetting = $this->invertWhenClause($settings[$fieldName]['when']);
+
+ $this->field($fieldName)->allowEmpty($whenSetting);
+ $this->_allowEmptyFlags[$fieldName] = static::EMPTY_ALL;
+ if ($settings[$fieldName]['message']) {
+ $this->_allowEmptyMessages[$fieldName] = $settings[$fieldName]['message'];
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Invert a when clause for creating notEmpty rules
+ *
+ * @param bool|string|callable $when Indicates when the field is not allowed
+ * to be empty. Valid values are true (always), 'create', 'update'. If a
+ * callable is passed then the field will allowed to be empty only when
+ * the callback returns false.
+ * @return bool|string|callable
+ */
+ protected function invertWhenClause($when)
+ {
+ if ($when === static::WHEN_CREATE || $when === static::WHEN_UPDATE) {
+ return $when === static::WHEN_CREATE ? static::WHEN_UPDATE : static::WHEN_CREATE;
+ }
+ if (is_callable($when)) {
+ return function ($context) use ($when) {
+ return !$when($context);
+ };
+ }
+
+ return $when;
+ }
+
+ /**
+ * Add a notBlank rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::notBlank()
+ * @return $this
+ */
+ public function notBlank(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'notBlank', $extra + [
+ 'rule' => 'notBlank',
+ ]);
+ }
+
+ /**
+ * Add an alphanumeric rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::alphaNumeric()
+ * @return $this
+ */
+ public function alphaNumeric(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'alphaNumeric', $extra + [
+ 'rule' => 'alphaNumeric',
+ ]);
+ }
+
+ /**
+ * Add a non-alphanumeric rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::notAlphaNumeric()
+ * @return $this
+ */
+ public function notAlphaNumeric(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'notAlphaNumeric', $extra + [
+ 'rule' => 'notAlphaNumeric',
+ ]);
+ }
+
+ /**
+ * Add an ascii-alphanumeric rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::asciiAlphaNumeric()
+ * @return $this
+ */
+ public function asciiAlphaNumeric(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'asciiAlphaNumeric', $extra + [
+ 'rule' => 'asciiAlphaNumeric',
+ ]);
+ }
+
+ /**
+ * Add a non-ascii alphanumeric rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::notAlphaNumeric()
+ * @return $this
+ */
+ public function notAsciiAlphaNumeric(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'notAsciiAlphaNumeric', $extra + [
+ 'rule' => 'notAsciiAlphaNumeric',
+ ]);
+ }
+
+ /**
+ * Add an rule that ensures a string length is within a range.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $range The inclusive minimum and maximum length you want permitted.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::alphaNumeric()
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function lengthBetween(string $field, array $range, ?string $message = null, $when = null)
+ {
+ if (count($range) !== 2) {
+ throw new InvalidArgumentException('The $range argument requires 2 numbers');
+ }
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'lengthBetween', $extra + [
+ 'rule' => ['lengthBetween', array_shift($range), array_shift($range)],
+ ]);
+ }
+
+ /**
+ * Add a credit card rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $type The type of cards you want to allow. Defaults to 'all'.
+ * You can also supply an array of accepted card types. e.g `['mastercard', 'visa', 'amex']`
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::creditCard()
+ * @return $this
+ */
+ public function creditCard(string $field, string $type = 'all', ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'creditCard', $extra + [
+ 'rule' => ['creditCard', $type, true],
+ ]);
+ }
+
+ /**
+ * Add a greater than comparison rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|float $value The value user data must be greater than.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::comparison()
+ * @return $this
+ */
+ public function greaterThan(string $field, $value, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'greaterThan', $extra + [
+ 'rule' => ['comparison', Validation::COMPARE_GREATER, $value],
+ ]);
+ }
+
+ /**
+ * Add a greater than or equal to comparison rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|float $value The value user data must be greater than or equal to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::comparison()
+ * @return $this
+ */
+ public function greaterThanOrEqual(string $field, $value, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'greaterThanOrEqual', $extra + [
+ 'rule' => ['comparison', Validation::COMPARE_GREATER_OR_EQUAL, $value],
+ ]);
+ }
+
+ /**
+ * Add a less than comparison rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|float $value The value user data must be less than.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::comparison()
+ * @return $this
+ */
+ public function lessThan(string $field, $value, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'lessThan', $extra + [
+ 'rule' => ['comparison', Validation::COMPARE_LESS, $value],
+ ]);
+ }
+
+ /**
+ * Add a less than or equal comparison rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|float $value The value user data must be less than or equal to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::comparison()
+ * @return $this
+ */
+ public function lessThanOrEqual(string $field, $value, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'lessThanOrEqual', $extra + [
+ 'rule' => ['comparison', Validation::COMPARE_LESS_OR_EQUAL, $value],
+ ]);
+ }
+
+ /**
+ * Add a equal to comparison rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|float $value The value user data must be equal to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::comparison()
+ * @return $this
+ */
+ public function equals(string $field, $value, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'equals', $extra + [
+ 'rule' => ['comparison', Validation::COMPARE_EQUAL, $value],
+ ]);
+ }
+
+ /**
+ * Add a not equal to comparison rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|float $value The value user data must be not be equal to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::comparison()
+ * @return $this
+ */
+ public function notEquals(string $field, $value, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'notEquals', $extra + [
+ 'rule' => ['comparison', Validation::COMPARE_NOT_EQUAL, $value],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare two fields to each other.
+ *
+ * If both fields have the exact same value the rule will pass.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ */
+ public function sameAs(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'sameAs', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_SAME],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare that two fields have different values.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function notSameAs(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'notSameAs', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_NOT_SAME],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare one field is equal to another.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function equalToField(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'equalToField', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_EQUAL],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare one field is not equal to another.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function notEqualToField(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'notEqualToField', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_NOT_EQUAL],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare one field is greater than another.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function greaterThanField(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'greaterThanField', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_GREATER],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare one field is greater than or equal to another.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function greaterThanOrEqualToField(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'greaterThanOrEqualToField', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_GREATER_OR_EQUAL],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare one field is less than another.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function lessThanField(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'lessThanField', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_LESS],
+ ]);
+ }
+
+ /**
+ * Add a rule to compare one field is less than or equal to another.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $secondField The field you want to compare against.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::compareFields()
+ * @return $this
+ * @since 3.6.0
+ */
+ public function lessThanOrEqualToField(string $field, string $secondField, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'lessThanOrEqualToField', $extra + [
+ 'rule' => ['compareFields', $secondField, Validation::COMPARE_LESS_OR_EQUAL],
+ ]);
+ }
+
+ /**
+ * Add a rule to check if a field contains non alpha numeric characters.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $limit The minimum number of non-alphanumeric fields required.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::containsNonAlphaNumeric()
+ * @return $this
+ * @deprecated 4.0.0 Use {@link notAlphaNumeric()} instead. Will be removed in 5.0
+ */
+ public function containsNonAlphaNumeric(string $field, int $limit = 1, ?string $message = null, $when = null)
+ {
+ deprecationWarning('Validator::containsNonAlphaNumeric() is deprecated. Use notAlphaNumeric() instead.');
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'containsNonAlphaNumeric', $extra + [
+ 'rule' => ['containsNonAlphaNumeric', $limit],
+ ]);
+ }
+
+ /**
+ * Add a date format validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $formats A list of accepted date formats.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::date()
+ * @return $this
+ */
+ public function date(string $field, array $formats = ['ymd'], ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'date', $extra + [
+ 'rule' => ['date', $formats],
+ ]);
+ }
+
+ /**
+ * Add a date time format validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $formats A list of accepted date formats.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::datetime()
+ * @return $this
+ */
+ public function dateTime(string $field, array $formats = ['ymd'], ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'dateTime', $extra + [
+ 'rule' => ['datetime', $formats],
+ ]);
+ }
+
+ /**
+ * Add a time format validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::time()
+ * @return $this
+ */
+ public function time(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'time', $extra + [
+ 'rule' => 'time',
+ ]);
+ }
+
+ /**
+ * Add a localized time, date or datetime format validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string $type Parser type, one out of 'date', 'time', and 'datetime'
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::localizedTime()
+ * @return $this
+ */
+ public function localizedTime(string $field, string $type = 'datetime', ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'localizedTime', $extra + [
+ 'rule' => ['localizedTime', $type],
+ ]);
+ }
+
+ /**
+ * Add a boolean validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::boolean()
+ * @return $this
+ */
+ public function boolean(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'boolean', $extra + [
+ 'rule' => 'boolean',
+ ]);
+ }
+
+ /**
+ * Add a decimal validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int|null $places The number of decimal places to require.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::decimal()
+ * @return $this
+ */
+ public function decimal(string $field, ?int $places = null, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'decimal', $extra + [
+ 'rule' => ['decimal', $places],
+ ]);
+ }
+
+ /**
+ * Add an email validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param bool $checkMX Whether or not to check the MX records.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::email()
+ * @return $this
+ */
+ public function email(string $field, bool $checkMX = false, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'email', $extra + [
+ 'rule' => ['email', $checkMX],
+ ]);
+ }
+
+ /**
+ * Add an IP validation rule to a field.
+ *
+ * This rule will accept both IPv4 and IPv6 addresses.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::ip()
+ * @return $this
+ */
+ public function ip(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'ip', $extra + [
+ 'rule' => 'ip',
+ ]);
+ }
+
+ /**
+ * Add an IPv4 validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::ip()
+ * @return $this
+ */
+ public function ipv4(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'ipv4', $extra + [
+ 'rule' => ['ip', 'ipv4'],
+ ]);
+ }
+
+ /**
+ * Add an IPv6 validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::ip()
+ * @return $this
+ */
+ public function ipv6(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'ipv6', $extra + [
+ 'rule' => ['ip', 'ipv6'],
+ ]);
+ }
+
+ /**
+ * Add a string length validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $min The minimum length required.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::minLength()
+ * @return $this
+ */
+ public function minLength(string $field, int $min, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'minLength', $extra + [
+ 'rule' => ['minLength', $min],
+ ]);
+ }
+
+ /**
+ * Add a string length validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $min The minimum length required.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::minLengthBytes()
+ * @return $this
+ */
+ public function minLengthBytes(string $field, int $min, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'minLengthBytes', $extra + [
+ 'rule' => ['minLengthBytes', $min],
+ ]);
+ }
+
+ /**
+ * Add a string length validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $max The maximum length allowed.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::maxLength()
+ * @return $this
+ */
+ public function maxLength(string $field, int $max, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'maxLength', $extra + [
+ 'rule' => ['maxLength', $max],
+ ]);
+ }
+
+ /**
+ * Add a string length validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $max The maximum length allowed.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::maxLengthBytes()
+ * @return $this
+ */
+ public function maxLengthBytes(string $field, int $max, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'maxLengthBytes', $extra + [
+ 'rule' => ['maxLengthBytes', $max],
+ ]);
+ }
+
+ /**
+ * Add a numeric value validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::numeric()
+ * @return $this
+ */
+ public function numeric(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'numeric', $extra + [
+ 'rule' => 'numeric',
+ ]);
+ }
+
+ /**
+ * Add a natural number validation rule to a field.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::naturalNumber()
+ * @return $this
+ */
+ public function naturalNumber(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'naturalNumber', $extra + [
+ 'rule' => ['naturalNumber', false],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field is a non negative integer.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::naturalNumber()
+ * @return $this
+ */
+ public function nonNegativeInteger(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'nonNegativeInteger', $extra + [
+ 'rule' => ['naturalNumber', true],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field is within a numeric range
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $range The inclusive upper and lower bounds of the valid range.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::range()
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function range(string $field, array $range, ?string $message = null, $when = null)
+ {
+ if (count($range) !== 2) {
+ throw new InvalidArgumentException('The $range argument requires 2 numbers');
+ }
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'range', $extra + [
+ 'rule' => ['range', array_shift($range), array_shift($range)],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field is a URL.
+ *
+ * This validator does not require a protocol.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::url()
+ * @return $this
+ */
+ public function url(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'url', $extra + [
+ 'rule' => ['url', false],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field is a URL.
+ *
+ * This validator requires the URL to have a protocol.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::url()
+ * @return $this
+ */
+ public function urlWithProtocol(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'urlWithProtocol', $extra + [
+ 'rule' => ['url', true],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure the field value is within an allowed list.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $list The list of valid options.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::inList()
+ * @return $this
+ */
+ public function inList(string $field, array $list, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'inList', $extra + [
+ 'rule' => ['inList', $list],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure the field is a UUID
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::uuid()
+ * @return $this
+ */
+ public function uuid(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'uuid', $extra + [
+ 'rule' => 'uuid',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure the field is an uploaded file
+ *
+ * For options see Cake\Validation\Validation::uploadedFile()
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $options An array of options.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::uploadedFile()
+ * @return $this
+ */
+ public function uploadedFile(string $field, array $options, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'uploadedFile', $extra + [
+ 'rule' => ['uploadedFile', $options],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure the field is a lat/long tuple.
+ *
+ * e.g. `, `
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::uuid()
+ * @return $this
+ */
+ public function latLong(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'latLong', $extra + [
+ 'rule' => 'geoCoordinate',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure the field is a latitude.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::latitude()
+ * @return $this
+ */
+ public function latitude(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'latitude', $extra + [
+ 'rule' => 'latitude',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure the field is a longitude.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::longitude()
+ * @return $this
+ */
+ public function longitude(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'longitude', $extra + [
+ 'rule' => 'longitude',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field contains only ascii bytes
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::ascii()
+ * @return $this
+ */
+ public function ascii(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'ascii', $extra + [
+ 'rule' => 'ascii',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field contains only BMP utf8 bytes
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::utf8()
+ * @return $this
+ */
+ public function utf8(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'utf8', $extra + [
+ 'rule' => ['utf8', ['extended' => false]],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field contains only utf8 bytes.
+ *
+ * This rule will accept 3 and 4 byte UTF8 sequences, which are necessary for emoji.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::utf8()
+ * @return $this
+ */
+ public function utf8Extended(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'utf8Extended', $extra + [
+ 'rule' => ['utf8', ['extended' => true]],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field is an integer value.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::isInteger()
+ * @return $this
+ */
+ public function integer(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'integer', $extra + [
+ 'rule' => 'isInteger',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure that a field contains an array.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::isArray()
+ * @return $this
+ */
+ public function isArray(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'isArray', $extra + [
+ 'rule' => 'isArray',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure that a field contains a scalar.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::isScalar()
+ * @return $this
+ */
+ public function scalar(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'scalar', $extra + [
+ 'rule' => 'isScalar',
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure a field is a 6 digits hex color value.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::hexColor()
+ * @return $this
+ */
+ public function hexColor(string $field, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'hexColor', $extra + [
+ 'rule' => 'hexColor',
+ ]);
+ }
+
+ /**
+ * Add a validation rule for a multiple select. Comparison is case sensitive by default.
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param array $options The options for the validator. Includes the options defined in
+ * \Cake\Validation\Validation::multiple() and the `caseInsensitive` parameter.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::multiple()
+ * @return $this
+ */
+ public function multipleOptions(string $field, array $options = [], ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+ $caseInsensitive = $options['caseInsensitive'] ?? false;
+ unset($options['caseInsensitive']);
+
+ return $this->add($field, 'multipleOptions', $extra + [
+ 'rule' => ['multiple', $options, $caseInsensitive],
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure that a field is an array containing at least
+ * the specified amount of elements
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $count The number of elements the array should at least have
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::numElements()
+ * @return $this
+ */
+ public function hasAtLeast(string $field, int $count, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'hasAtLeast', $extra + [
+ 'rule' => function ($value) use ($count) {
+ if (is_array($value) && isset($value['_ids'])) {
+ $value = $value['_ids'];
+ }
+
+ return Validation::numElements($value, Validation::COMPARE_GREATER_OR_EQUAL, $count);
+ },
+ ]);
+ }
+
+ /**
+ * Add a validation rule to ensure that a field is an array containing at most
+ * the specified amount of elements
+ *
+ * @param string $field The field you want to apply the rule to.
+ * @param int $count The number maximum amount of elements the field should have
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @see \Cake\Validation\Validation::numElements()
+ * @return $this
+ */
+ public function hasAtMost(string $field, int $count, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'hasAtMost', $extra + [
+ 'rule' => function ($value) use ($count) {
+ if (is_array($value) && isset($value['_ids'])) {
+ $value = $value['_ids'];
+ }
+
+ return Validation::numElements($value, Validation::COMPARE_LESS_OR_EQUAL, $count);
+ },
+ ]);
+ }
+
+ /**
+ * Returns whether or not a field can be left empty for a new or already existing
+ * record.
+ *
+ * @param string $field Field name.
+ * @param bool $newRecord whether the data to be validated is new or to be updated.
+ * @return bool
+ */
+ public function isEmptyAllowed(string $field, bool $newRecord): bool
+ {
+ $providers = $this->_providers;
+ $data = [];
+ $context = compact('data', 'newRecord', 'field', 'providers');
+
+ return $this->_canBeEmpty($this->field($field), $context);
+ }
+
+ /**
+ * Returns whether or not a field can be left out for a new or already existing
+ * record.
+ *
+ * @param string $field Field name.
+ * @param bool $newRecord Whether the data to be validated is new or to be updated.
+ * @return bool
+ */
+ public function isPresenceRequired(string $field, bool $newRecord): bool
+ {
+ $providers = $this->_providers;
+ $data = [];
+ $context = compact('data', 'newRecord', 'field', 'providers');
+
+ return !$this->_checkPresence($this->field($field), $context);
+ }
+
+ /**
+ * Returns whether or not a field matches against a regular expression.
+ *
+ * @param string $field Field name.
+ * @param string $regex Regular expression.
+ * @param string|null $message The error message when the rule fails.
+ * @param string|callable|null $when Either 'create' or 'update' or a callable that returns
+ * true when the validation rule should be applied.
+ * @return $this
+ */
+ public function regex(string $field, string $regex, ?string $message = null, $when = null)
+ {
+ $extra = array_filter(['on' => $when, 'message' => $message]);
+
+ return $this->add($field, 'regex', $extra + [
+ 'rule' => ['custom', $regex],
+ ]);
+ }
+
+ /**
+ * Gets the required message for a field
+ *
+ * @param string $field Field name
+ * @return string|null
+ */
+ public function getRequiredMessage(string $field): ?string
+ {
+ if (!isset($this->_fields[$field])) {
+ return null;
+ }
+
+ $defaultMessage = 'This field is required';
+ if ($this->_useI18n) {
+ $defaultMessage = __d('cake', 'This field is required');
+ }
+
+ return $this->_presenceMessages[$field] ?? $defaultMessage;
+ }
+
+ /**
+ * Gets the notEmpty message for a field
+ *
+ * @param string $field Field name
+ * @return string|null
+ */
+ public function getNotEmptyMessage(string $field): ?string
+ {
+ if (!isset($this->_fields[$field])) {
+ return null;
+ }
+
+ $defaultMessage = 'This field cannot be left empty';
+ if ($this->_useI18n) {
+ $defaultMessage = __d('cake', 'This field cannot be left empty');
+ }
+
+ foreach ($this->_fields[$field] as $rule) {
+ if ($rule->get('rule') === 'notBlank' && $rule->get('message')) {
+ return $rule->get('message');
+ }
+ }
+
+ return $this->_allowEmptyMessages[$field] ?? $defaultMessage;
+ }
+
+ /**
+ * Returns false if any validation for the passed rule set should be stopped
+ * due to the field missing in the data array
+ *
+ * @param \Cake\Validation\ValidationSet $field The set of rules for a field.
+ * @param array $context A key value list of data containing the validation context.
+ * @return bool
+ */
+ protected function _checkPresence(ValidationSet $field, array $context): bool
+ {
+ $required = $field->isPresenceRequired();
+
+ if (!is_string($required) && is_callable($required)) {
+ return !$required($context);
+ }
+
+ $newRecord = $context['newRecord'];
+ if (in_array($required, [static::WHEN_CREATE, static::WHEN_UPDATE], true)) {
+ return ($required === static::WHEN_CREATE && !$newRecord) ||
+ ($required === static::WHEN_UPDATE && $newRecord);
+ }
+
+ return !$required;
+ }
+
+ /**
+ * Returns whether the field can be left blank according to `allowEmpty`
+ *
+ * @param \Cake\Validation\ValidationSet $field the set of rules for a field
+ * @param array $context a key value list of data containing the validation context.
+ * @return bool
+ */
+ protected function _canBeEmpty(ValidationSet $field, array $context): bool
+ {
+ $allowed = $field->isEmptyAllowed();
+
+ if (!is_string($allowed) && is_callable($allowed)) {
+ return $allowed($context);
+ }
+
+ $newRecord = $context['newRecord'];
+ if (in_array($allowed, [static::WHEN_CREATE, static::WHEN_UPDATE], true)) {
+ $allowed = ($allowed === static::WHEN_CREATE && $newRecord) ||
+ ($allowed === static::WHEN_UPDATE && !$newRecord);
+ }
+
+ return (bool)$allowed;
+ }
+
+ /**
+ * Returns true if the field is empty in the passed data array
+ *
+ * @param mixed $data Value to check against.
+ * @return bool
+ * @deprecated 3.7.0 Use {@link isEmpty()} instead
+ */
+ protected function _fieldIsEmpty($data): bool
+ {
+ return $this->isEmpty($data, static::EMPTY_ALL);
+ }
+
+ /**
+ * Returns true if the field is empty in the passed data array
+ *
+ * @param mixed $data Value to check against.
+ * @param int $flags A bitmask of EMPTY_* flags which specify what is empty
+ * @return bool
+ */
+ protected function isEmpty($data, int $flags): bool
+ {
+ if ($data === null) {
+ return true;
+ }
+
+ if ($data === '' && ($flags & self::EMPTY_STRING)) {
+ return true;
+ }
+
+ $arrayTypes = self::EMPTY_ARRAY | self::EMPTY_DATE | self::EMPTY_TIME;
+ if ($data === [] && ($flags & $arrayTypes)) {
+ return true;
+ }
+
+ if (is_array($data)) {
+ if (
+ ($flags & self::EMPTY_FILE)
+ && isset($data['name'], $data['type'], $data['tmp_name'], $data['error'])
+ && (int)$data['error'] === UPLOAD_ERR_NO_FILE
+ ) {
+ return true;
+ }
+
+ $allFieldsAreEmpty = true;
+ foreach ($data as $field) {
+ if ($field !== null && $field !== '') {
+ $allFieldsAreEmpty = false;
+ break;
+ }
+ }
+
+ if ($allFieldsAreEmpty) {
+ if (($flags & self::EMPTY_DATE) && isset($data['year'])) {
+ return true;
+ }
+
+ if (($flags & self::EMPTY_TIME) && isset($data['hour'])) {
+ return true;
+ }
+ }
+ }
+
+ if (
+ ($flags & self::EMPTY_FILE)
+ && $data instanceof UploadedFileInterface
+ && $data->getError() === UPLOAD_ERR_NO_FILE
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Iterates over each rule in the validation set and collects the errors resulting
+ * from executing them
+ *
+ * @param string $field The name of the field that is being processed
+ * @param \Cake\Validation\ValidationSet $rules the list of rules for a field
+ * @param array $data the full data passed to the validator
+ * @param bool $newRecord whether is it a new record or an existing one
+ * @return array
+ */
+ protected function _processRules(string $field, ValidationSet $rules, array $data, bool $newRecord): array
+ {
+ $errors = [];
+ // Loading default provider in case there is none
+ $this->getProvider('default');
+ $message = 'The provided value is invalid';
+
+ if ($this->_useI18n) {
+ $message = __d('cake', 'The provided value is invalid');
+ }
+
+ foreach ($rules as $name => $rule) {
+ $result = $rule->process($data[$field], $this->_providers, compact('newRecord', 'data', 'field'));
+ if ($result === true) {
+ continue;
+ }
+
+ $errors[$name] = $message;
+ if (is_array($result) && $name === static::NESTED) {
+ $errors = $result;
+ }
+ if (is_string($result)) {
+ $errors[$name] = $result;
+ }
+
+ if ($rule->isLast()) {
+ break;
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Get the printable version of this object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ $fields = [];
+ foreach ($this->_fields as $name => $fieldSet) {
+ $fields[$name] = [
+ 'isPresenceRequired' => $fieldSet->isPresenceRequired(),
+ 'isEmptyAllowed' => $fieldSet->isEmptyAllowed(),
+ 'rules' => array_keys($fieldSet->rules()),
+ ];
+ }
+
+ return [
+ '_presenceMessages' => $this->_presenceMessages,
+ '_allowEmptyMessages' => $this->_allowEmptyMessages,
+ '_allowEmptyFlags' => $this->_allowEmptyFlags,
+ '_useI18n' => $this->_useI18n,
+ '_stopOnFailure' => $this->_stopOnFailure,
+ '_providers' => array_keys($this->_providers),
+ '_fields' => $fields,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/ValidatorAwareInterface.php b/app/vendor/cakephp/cakephp/src/Validation/ValidatorAwareInterface.php
new file mode 100644
index 000000000..7d2d52dfd
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/ValidatorAwareInterface.php
@@ -0,0 +1,52 @@
+add('email', 'valid-email', ['rule' => 'email'])
+ * ->add('password', 'valid', ['rule' => 'notBlank'])
+ * ->requirePresence('username');
+ * }
+ * $validator = $this->getValidator('forSubscription');
+ * ```
+ *
+ * You can implement the method in `validationDefault` in your Table subclass
+ * should you wish to have a validation set that applies in cases where no other
+ * set is specified.
+ *
+ * If a $name argument has not been provided, the default validator will be returned.
+ * You can configure your default validator name in a `DEFAULT_VALIDATOR`
+ * class constant.
+ *
+ * @param string|null $name The name of the validation set to return.
+ * @return \Cake\Validation\Validator
+ */
+ public function getValidator(?string $name = null): Validator
+ {
+ $name = $name ?: static::DEFAULT_VALIDATOR;
+ if (!isset($this->_validators[$name])) {
+ $this->setValidator($name, $this->createValidator($name));
+ }
+
+ return $this->_validators[$name];
+ }
+
+ /**
+ * Creates a validator using a custom method inside your class.
+ *
+ * This method is used only to build a new validator and it does not store
+ * it in your object. If you want to build and reuse validators,
+ * use getValidator() method instead.
+ *
+ * @param string $name The name of the validation set to create.
+ * @return \Cake\Validation\Validator
+ * @throws \RuntimeException
+ */
+ protected function createValidator(string $name): Validator
+ {
+ $method = 'validation' . ucfirst($name);
+ if (!$this->validationMethodExists($method)) {
+ $message = sprintf('The %s::%s() validation method does not exists.', static::class, $method);
+ throw new RuntimeException($message);
+ }
+
+ $validator = new $this->_validatorClass();
+ $validator = $this->$method($validator);
+ if ($this instanceof EventDispatcherInterface) {
+ $event = defined(static::class . '::BUILD_VALIDATOR_EVENT')
+ ? static::BUILD_VALIDATOR_EVENT
+ : 'Model.buildValidator';
+ $this->dispatchEvent($event, compact('validator', 'name'));
+ }
+
+ if (!$validator instanceof Validator) {
+ throw new RuntimeException(sprintf(
+ 'The %s::%s() validation method must return an instance of %s.',
+ static::class,
+ $method,
+ Validator::class
+ ));
+ }
+
+ return $validator;
+ }
+
+ /**
+ * This method stores a custom validator under the given name.
+ *
+ * You can build the object by yourself and store it in your object:
+ *
+ * ```
+ * $validator = new \Cake\Validation\Validator($table);
+ * $validator
+ * ->add('email', 'valid-email', ['rule' => 'email'])
+ * ->add('password', 'valid', ['rule' => 'notBlank'])
+ * ->allowEmpty('bio');
+ * $this->setValidator('forSubscription', $validator);
+ * ```
+ *
+ * @param string $name The name of a validator to be set.
+ * @param \Cake\Validation\Validator $validator Validator object to be set.
+ * @return $this
+ */
+ public function setValidator(string $name, Validator $validator)
+ {
+ $validator->setProvider(static::VALIDATOR_PROVIDER_NAME, $this);
+ $this->_validators[$name] = $validator;
+
+ return $this;
+ }
+
+ /**
+ * Checks whether or not a validator has been set.
+ *
+ * @param string $name The name of a validator.
+ * @return bool
+ */
+ public function hasValidator(string $name): bool
+ {
+ $method = 'validation' . ucfirst($name);
+ if ($this->validationMethodExists($method)) {
+ return true;
+ }
+
+ return isset($this->_validators[$name]);
+ }
+
+ /**
+ * Checks if validation method exists.
+ *
+ * @param string $name Validation method name.
+ * @return bool
+ */
+ protected function validationMethodExists(string $name): bool
+ {
+ return method_exists($this, $name);
+ }
+
+ /**
+ * Returns the default validator object. Subclasses can override this function
+ * to add a default validation set to the validator object.
+ *
+ * @param \Cake\Validation\Validator $validator The validator that can be modified to
+ * add some rules to it.
+ * @return \Cake\Validation\Validator
+ */
+ public function validationDefault(Validator $validator): Validator
+ {
+ return $validator;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Validation/composer.json b/app/vendor/cakephp/cakephp/src/Validation/composer.json
new file mode 100644
index 000000000..eceaafd2e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/Validation/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "cakephp/validation",
+ "description": "CakePHP Validation library",
+ "type": "library",
+ "keywords": [
+ "cakephp",
+ "validation",
+ "data validation"
+ ],
+ "homepage": "https://cakephp.org",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/validation/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/validation"
+ },
+ "require": {
+ "php": ">=7.2.0",
+ "cakephp/core": "^4.0",
+ "cakephp/utility": "^4.0",
+ "psr/http-message": "^1.0.0"
+ },
+ "suggest": {
+ "cakephp/i18n": "If you want to use Validation::localizedTime()"
+ },
+ "autoload": {
+ "psr-4": {
+ "Cake\\Validation\\": "."
+ }
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/AjaxView.php b/app/vendor/cakephp/cakephp/src/View/AjaxView.php
new file mode 100644
index 000000000..f0fa4ae49
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/AjaxView.php
@@ -0,0 +1,39 @@
+setResponse($this->getResponse()->withType('ajax'));
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Cell.php b/app/vendor/cakephp/cakephp/src/View/Cell.php
new file mode 100644
index 000000000..774854c01
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Cell.php
@@ -0,0 +1,299 @@
+setEventManager($eventManager);
+ }
+ $this->request = $request;
+ $this->response = $response;
+ $this->modelFactory('Table', [$this->getTableLocator(), 'get']);
+
+ $this->_validCellOptions = array_merge(['action', 'args'], $this->_validCellOptions);
+ foreach ($this->_validCellOptions as $var) {
+ if (isset($cellOptions[$var])) {
+ $this->{$var} = $cellOptions[$var];
+ }
+ }
+ if (!empty($cellOptions['cache'])) {
+ $this->_cache = $cellOptions['cache'];
+ }
+
+ $this->initialize();
+ }
+
+ /**
+ * Initialization hook method.
+ *
+ * Implement this method to avoid having to overwrite
+ * the constructor and calling parent::__construct().
+ *
+ * @return void
+ */
+ public function initialize(): void
+ {
+ }
+
+ /**
+ * Render the cell.
+ *
+ * @param string|null $template Custom template name to render. If not provided (null), the last
+ * value will be used. This value is automatically set by `CellTrait::cell()`.
+ * @return string The rendered cell.
+ * @throws \Cake\View\Exception\MissingCellTemplateException
+ * When a MissingTemplateException is raised during rendering.
+ */
+ public function render(?string $template = null): string
+ {
+ $cache = [];
+ if ($this->_cache) {
+ $cache = $this->_cacheConfig($this->action, $template);
+ }
+
+ $render = function () use ($template) {
+ try {
+ $reflect = new ReflectionMethod($this, $this->action);
+ $reflect->invokeArgs($this, $this->args);
+ } catch (ReflectionException $e) {
+ throw new BadMethodCallException(sprintf(
+ 'Class %s does not have a "%s" method.',
+ static::class,
+ $this->action
+ ));
+ }
+
+ $builder = $this->viewBuilder();
+
+ if ($template !== null) {
+ $builder->setTemplate($template);
+ }
+
+ $className = static::class;
+ $namePrefix = '\View\Cell\\';
+ /** @psalm-suppress PossiblyFalseOperand */
+ $name = substr($className, strpos($className, $namePrefix) + strlen($namePrefix));
+ $name = substr($name, 0, -4);
+ if (!$builder->getTemplatePath()) {
+ $builder->setTemplatePath(
+ static::TEMPLATE_FOLDER . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $name)
+ );
+ }
+ $template = $builder->getTemplate();
+
+ $view = $this->createView();
+ try {
+ return $view->render($template, false);
+ } catch (MissingTemplateException $e) {
+ $attributes = $e->getAttributes();
+ throw new MissingCellTemplateException(
+ $name,
+ $attributes['file'],
+ $attributes['paths'],
+ null,
+ $e
+ );
+ }
+ };
+
+ if ($cache) {
+ return Cache::remember($cache['key'], $render, $cache['config']);
+ }
+
+ return $render();
+ }
+
+ /**
+ * Generate the cache key to use for this cell.
+ *
+ * If the key is undefined, the cell class and action name will be used.
+ *
+ * @param string $action The action invoked.
+ * @param string|null $template The name of the template to be rendered.
+ * @return array The cache configuration.
+ */
+ protected function _cacheConfig(string $action, ?string $template = null): array
+ {
+ if (empty($this->_cache)) {
+ return [];
+ }
+ $template = $template ?: 'default';
+ $key = 'cell_' . Inflector::underscore(static::class) . '_' . $action . '_' . $template;
+ $key = str_replace('\\', '_', $key);
+ $default = [
+ 'config' => 'default',
+ 'key' => $key,
+ ];
+ if ($this->_cache === true) {
+ return $default;
+ }
+
+ /** @psalm-suppress PossiblyFalseOperand */
+ return $this->_cache + $default;
+ }
+
+ /**
+ * Magic method.
+ *
+ * Starts the rendering process when Cell is echoed.
+ *
+ * *Note* This method will trigger an error when view rendering has a problem.
+ * This is because PHP will not allow a __toString() method to throw an exception.
+ *
+ * @return string Rendered cell
+ * @throws \Error Include error details for PHP 7 fatal errors.
+ */
+ public function __toString(): string
+ {
+ try {
+ return $this->render();
+ } catch (Exception $e) {
+ trigger_error(sprintf(
+ 'Could not render cell - %s [%s, line %d]',
+ $e->getMessage(),
+ $e->getFile(),
+ $e->getLine()
+ ), E_USER_WARNING);
+
+ return '';
+ } catch (Error $e) {
+ throw new Error(sprintf(
+ 'Could not render cell - %s [%s, line %d]',
+ $e->getMessage(),
+ $e->getFile(),
+ $e->getLine()
+ ), 0, $e);
+ }
+ }
+
+ /**
+ * Debug info.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'action' => $this->action,
+ 'args' => $this->args,
+ 'request' => $this->request,
+ 'response' => $this->response,
+ 'viewBuilder' => $this->viewBuilder(),
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/CellTrait.php b/app/vendor/cakephp/cakephp/src/View/CellTrait.php
new file mode 100644
index 000000000..16d465d5c
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/CellTrait.php
@@ -0,0 +1,131 @@
+cell('Taxonomy.TagCloud::smallList', ['limit' => 10]);
+ *
+ * // App\View\Cell\TagCloudCell::smallList()
+ * $cell = $this->cell('TagCloud::smallList', ['limit' => 10]);
+ * ```
+ *
+ * The `display` action will be used by default when no action is provided:
+ *
+ * ```
+ * // Taxonomy\View\Cell\TagCloudCell::display()
+ * $cell = $this->cell('Taxonomy.TagCloud');
+ * ```
+ *
+ * Cells are not rendered until they are echoed.
+ *
+ * @param string $cell You must indicate cell name, and optionally a cell action. e.g.: `TagCloud::smallList` will
+ * invoke `View\Cell\TagCloudCell::smallList()`, `display` action will be invoked by default when none is provided.
+ * @param array $data Additional arguments for cell method. e.g.:
+ * `cell('TagCloud::smallList', ['a1' => 'v1', 'a2' => 'v2'])` maps to `View\Cell\TagCloud::smallList(v1, v2)`
+ * @param array $options Options for Cell's constructor
+ * @return \Cake\View\Cell The cell instance
+ * @throws \Cake\View\Exception\MissingCellException If Cell class was not found.
+ * @throws \BadMethodCallException If Cell class does not specified cell action.
+ */
+ protected function cell(string $cell, array $data = [], array $options = []): Cell
+ {
+ $parts = explode('::', $cell);
+
+ if (count($parts) === 2) {
+ [$pluginAndCell, $action] = [$parts[0], $parts[1]];
+ } else {
+ [$pluginAndCell, $action] = [$parts[0], 'display'];
+ }
+
+ [$plugin] = pluginSplit($pluginAndCell);
+ $className = App::className($pluginAndCell, 'View/Cell', 'Cell');
+
+ if (!$className) {
+ throw new MissingCellException(['className' => $pluginAndCell . 'Cell']);
+ }
+
+ if (!empty($data)) {
+ $data = array_values($data);
+ }
+ $options = ['action' => $action, 'args' => $data] + $options;
+ $cell = $this->_createCell($className, $action, $plugin, $options);
+
+ return $cell;
+ }
+
+ /**
+ * Create and configure the cell instance.
+ *
+ * @param string $className The cell classname.
+ * @param string $action The action name.
+ * @param string|null $plugin The plugin name.
+ * @param array $options The constructor options for the cell.
+ * @return \Cake\View\Cell
+ */
+ protected function _createCell(string $className, string $action, ?string $plugin, array $options): Cell
+ {
+ /** @var \Cake\View\Cell $instance */
+ $instance = new $className($this->request, $this->response, $this->getEventManager(), $options);
+
+ $builder = $instance->viewBuilder();
+ $builder->setTemplate(Inflector::underscore($action));
+
+ if (!empty($plugin)) {
+ $builder->setPlugin($plugin);
+ }
+ if (!empty($this->helpers)) {
+ $builder->setHelpers($this->helpers);
+ }
+
+ if ($this instanceof View) {
+ if (!empty($this->theme)) {
+ $builder->setTheme($this->theme);
+ }
+
+ $class = static::class;
+ $builder->setClassName($class);
+ $instance->viewBuilder()->setClassName($class);
+
+ return $instance;
+ }
+
+ if (method_exists($this, 'viewBuilder')) {
+ $builder->setTheme($this->viewBuilder()->getTheme());
+
+ if ($this->viewBuilder()->getClassName() !== null) {
+ $builder->setClassName($this->viewBuilder()->getClassName());
+ }
+ }
+
+ return $instance;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Exception/MissingCellException.php b/app/vendor/cakephp/cakephp/src/View/Exception/MissingCellException.php
new file mode 100644
index 000000000..3f473c831
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Exception/MissingCellException.php
@@ -0,0 +1,30 @@
+name = $name;
+
+ parent::__construct($file, $paths, $code, $previous);
+ }
+
+ /**
+ * Get the passed in attributes
+ *
+ * @return array
+ * @psalm-return array{name: string, file: string, paths: array}
+ */
+ public function getAttributes(): array
+ {
+ return [
+ 'name' => $this->name,
+ 'file' => $this->file,
+ 'paths' => $this->paths,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Exception/MissingElementException.php b/app/vendor/cakephp/cakephp/src/View/Exception/MissingElementException.php
new file mode 100644
index 000000000..92aa91ccb
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Exception/MissingElementException.php
@@ -0,0 +1,26 @@
+file = is_array($file) ? array_pop($file) : $file;
+ $this->paths = $paths;
+
+ parent::__construct($this->formatMessage(), $code, $previous);
+ }
+
+ /**
+ * Get the formatted exception message.
+ *
+ * @return string
+ */
+ public function formatMessage(): string
+ {
+ $message = "{$this->type} file `{$this->file}` could not be found.";
+ if ($this->paths) {
+ $message .= "\n\nThe following paths were searched:\n\n";
+ foreach ($this->paths as $path) {
+ $message .= "- `{$path}{$this->file}`\n";
+ }
+ }
+
+ return $message;
+ }
+
+ /**
+ * Get the passed in attributes
+ *
+ * @return array
+ * @psalm-return array{file: string, paths: array}
+ */
+ public function getAttributes(): array
+ {
+ return [
+ 'file' => $this->file,
+ 'paths' => $this->paths,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Exception/MissingViewException.php b/app/vendor/cakephp/cakephp/src/View/Exception/MissingViewException.php
new file mode 100644
index 000000000..da0a3774d
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Exception/MissingViewException.php
@@ -0,0 +1,30 @@
+ [
+ * 'id' => '1',
+ * 'title' => 'First post!',
+ * ],
+ * 'schema' => [
+ * 'id' => ['type' => 'integer'],
+ * 'title' => ['type' => 'string', 'length' => 255],
+ * '_constraints' => [
+ * 'primary' => ['type' => 'primary', 'columns' => ['id']]
+ * ]
+ * ],
+ * 'defaults' => [
+ * 'title' => 'Default title',
+ * ],
+ * 'required' => [
+ * 'id' => true, // will use default required message
+ * 'title' => 'Please enter a title',
+ * 'body' => false,
+ * ],
+ * ];
+ * ```
+ */
+class ArrayContext implements ContextInterface
+{
+ /**
+ * Context data for this object.
+ *
+ * @var array
+ */
+ protected $_context;
+
+ /**
+ * Constructor.
+ *
+ * @param array $context Context info.
+ */
+ public function __construct(array $context)
+ {
+ $context += [
+ 'data' => [],
+ 'schema' => [],
+ 'required' => [],
+ 'defaults' => [],
+ 'errors' => [],
+ ];
+ $this->_context = $context;
+ }
+
+ /**
+ * Get the fields used in the context as a primary key.
+ *
+ * @return string[]
+ * @deprecated 4.0.0 Renamed to {@link getPrimaryKey()}.
+ */
+ public function primaryKey(): array
+ {
+ deprecationWarning('`ArrayContext::primaryKey()` is deprecated. Use `ArrayContext::getPrimaryKey()`.');
+
+ return $this->getPrimaryKey();
+ }
+
+ /**
+ * Get the fields used in the context as a primary key.
+ *
+ * @return string[]
+ */
+ public function getPrimaryKey(): array
+ {
+ if (
+ empty($this->_context['schema']['_constraints']) ||
+ !is_array($this->_context['schema']['_constraints'])
+ ) {
+ return [];
+ }
+ foreach ($this->_context['schema']['_constraints'] as $data) {
+ if (isset($data['type']) && $data['type'] === 'primary') {
+ return (array)($data['columns'] ?? []);
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isPrimaryKey(string $field): bool
+ {
+ $primaryKey = $this->getPrimaryKey();
+
+ return in_array($field, $primaryKey, true);
+ }
+
+ /**
+ * Returns whether or not this form is for a create operation.
+ *
+ * For this method to return true, both the primary key constraint
+ * must be defined in the 'schema' data, and the 'defaults' data must
+ * contain a value for all fields in the key.
+ *
+ * @return bool
+ */
+ public function isCreate(): bool
+ {
+ $primary = $this->getPrimaryKey();
+ foreach ($primary as $column) {
+ if (!empty($this->_context['defaults'][$column])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the current value for a given field.
+ *
+ * This method will coalesce the current data and the 'defaults' array.
+ *
+ * @param string $field A dot separated path to the field a value
+ * is needed for.
+ * @param array $options Options:
+ *
+ * - `default`: Default value to return if no value found in data or
+ * context record.
+ * - `schemaDefault`: Boolean indicating whether default value from
+ * context's schema should be used if it's not explicitly provided.
+ * @return mixed
+ */
+ public function val(string $field, array $options = [])
+ {
+ $options += [
+ 'default' => null,
+ 'schemaDefault' => true,
+ ];
+
+ if (Hash::check($this->_context['data'], $field)) {
+ return Hash::get($this->_context['data'], $field);
+ }
+
+ if ($options['default'] !== null || !$options['schemaDefault']) {
+ return $options['default'];
+ }
+ if (empty($this->_context['defaults']) || !is_array($this->_context['defaults'])) {
+ return null;
+ }
+
+ // Using Hash::check here incase the default value is actually null
+ if (Hash::check($this->_context['defaults'], $field)) {
+ return Hash::get($this->_context['defaults'], $field);
+ }
+
+ return Hash::get($this->_context['defaults'], $this->stripNesting($field));
+ }
+
+ /**
+ * Check if a given field is 'required'.
+ *
+ * In this context class, this is simply defined by the 'required' array.
+ *
+ * @param string $field A dot separated path to check required-ness for.
+ * @return bool|null
+ */
+ public function isRequired(string $field): ?bool
+ {
+ if (!is_array($this->_context['required'])) {
+ return null;
+ }
+
+ $required = Hash::get($this->_context['required'], $field);
+
+ if ($required === null) {
+ $required = Hash::get($this->_context['required'], $this->stripNesting($field));
+ }
+
+ if (!empty($required) || $required === '0') {
+ return true;
+ }
+
+ return $required;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRequiredMessage(string $field): ?string
+ {
+ if (!is_array($this->_context['required'])) {
+ return null;
+ }
+ $required = Hash::get($this->_context['required'], $field);
+ if ($required === null) {
+ $required = Hash::get($this->_context['required'], $this->stripNesting($field));
+ }
+
+ if ($required === false) {
+ return null;
+ }
+
+ if ($required === true) {
+ $required = __d('cake', 'This field cannot be left empty');
+ }
+
+ return $required;
+ }
+
+ /**
+ * Get field length from validation
+ *
+ * In this context class, this is simply defined by the 'length' array.
+ *
+ * @param string $field A dot separated path to check required-ness for.
+ * @return int|null
+ */
+ public function getMaxLength(string $field): ?int
+ {
+ if (!is_array($this->_context['schema'])) {
+ return null;
+ }
+
+ return Hash::get($this->_context['schema'], "$field.length");
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fieldNames(): array
+ {
+ $schema = $this->_context['schema'];
+ unset($schema['_constraints'], $schema['_indexes']);
+
+ return array_keys($schema);
+ }
+
+ /**
+ * Get the abstract field type for a given field name.
+ *
+ * @param string $field A dot separated path to get a schema type for.
+ * @return string|null An abstract data type or null.
+ * @see \Cake\Database\TypeFactory
+ */
+ public function type(string $field): ?string
+ {
+ if (!is_array($this->_context['schema'])) {
+ return null;
+ }
+
+ $schema = Hash::get($this->_context['schema'], $field);
+ if ($schema === null) {
+ $schema = Hash::get($this->_context['schema'], $this->stripNesting($field));
+ }
+
+ return $schema['type'] ?? null;
+ }
+
+ /**
+ * Get an associative array of other attributes for a field name.
+ *
+ * @param string $field A dot separated path to get additional data on.
+ * @return array An array of data describing the additional attributes on a field.
+ */
+ public function attributes(string $field): array
+ {
+ if (!is_array($this->_context['schema'])) {
+ return [];
+ }
+ $schema = Hash::get($this->_context['schema'], $field);
+ if ($schema === null) {
+ $schema = Hash::get($this->_context['schema'], $this->stripNesting($field));
+ }
+
+ return array_intersect_key(
+ (array)$schema,
+ array_flip(static::VALID_ATTRIBUTES)
+ );
+ }
+
+ /**
+ * Check whether or not a field has an error attached to it
+ *
+ * @param string $field A dot separated path to check errors on.
+ * @return bool Returns true if the errors for the field are not empty.
+ */
+ public function hasError(string $field): bool
+ {
+ if (empty($this->_context['errors'])) {
+ return false;
+ }
+
+ return Hash::check($this->_context['errors'], $field);
+ }
+
+ /**
+ * Get the errors for a given field
+ *
+ * @param string $field A dot separated path to check errors on.
+ * @return array An array of errors, an empty array will be returned when the
+ * context has no errors.
+ */
+ public function error(string $field): array
+ {
+ if (empty($this->_context['errors'])) {
+ return [];
+ }
+
+ return (array)Hash::get($this->_context['errors'], $field);
+ }
+
+ /**
+ * Strips out any numeric nesting
+ *
+ * For example users.0.age will output as users.age
+ *
+ * @param string $field A dot separated path
+ * @return string A string with stripped numeric nesting
+ */
+ protected function stripNesting(string $field): string
+ {
+ return preg_replace('/\.\d*\./', '.', $field);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Form/ContextFactory.php b/app/vendor/cakephp/cakephp/src/View/Form/ContextFactory.php
new file mode 100644
index 000000000..490f2aee1
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Form/ContextFactory.php
@@ -0,0 +1,163 @@
+ 'a-string', 'callable' => ..]`
+ */
+ public function __construct(array $providers = [])
+ {
+ foreach ($providers as $provider) {
+ $this->addProvider($provider['type'], $provider['callable']);
+ }
+ }
+
+ /**
+ * Create factory instance with providers "array", "form" and "orm".
+ *
+ * @param array $providers Array of provider callables. Each element should
+ * be of form `['type' => 'a-string', 'callable' => ..]`
+ * @return static
+ */
+ public static function createWithDefaults(array $providers = [])
+ {
+ $providers = [
+ [
+ 'type' => 'orm',
+ 'callable' => function ($request, $data) {
+ if ($data['entity'] instanceof EntityInterface) {
+ return new EntityContext($data);
+ }
+ if (isset($data['table'])) {
+ return new EntityContext($data);
+ }
+ if (is_iterable($data['entity'])) {
+ $pass = (new Collection($data['entity']))->first() !== null;
+ if ($pass) {
+ return new EntityContext($data);
+ } else {
+ return new NullContext($data);
+ }
+ }
+ },
+ ],
+ [
+ 'type' => 'form',
+ 'callable' => function ($request, $data) {
+ if ($data['entity'] instanceof Form) {
+ return new FormContext($data);
+ }
+ },
+ ],
+ [
+ 'type' => 'array',
+ 'callable' => function ($request, $data) {
+ if (is_array($data['entity']) && isset($data['entity']['schema'])) {
+ return new ArrayContext($data['entity']);
+ }
+ },
+ ],
+ [
+ 'type' => 'null',
+ 'callable' => function ($request, $data) {
+ if ($data['entity'] === null) {
+ return new NullContext($data);
+ }
+ },
+ ],
+ ] + $providers;
+
+ return new static($providers);
+ }
+
+ /**
+ * Add a new context type.
+ *
+ * Form context types allow FormHelper to interact with
+ * data providers that come from outside CakePHP. For example
+ * if you wanted to use an alternative ORM like Doctrine you could
+ * create and connect a new context class to allow FormHelper to
+ * read metadata from doctrine.
+ *
+ * @param string $type The type of context. This key
+ * can be used to overwrite existing providers.
+ * @param callable $check A callable that returns an object
+ * when the form context is the correct type.
+ * @return $this
+ */
+ public function addProvider(string $type, callable $check)
+ {
+ $this->providers = [$type => ['type' => $type, 'callable' => $check]]
+ + $this->providers;
+
+ return $this;
+ }
+
+ /**
+ * Find the matching context for the data.
+ *
+ * If no type can be matched a NullContext will be returned.
+ *
+ * @param \Cake\Http\ServerRequest $request Request instance.
+ * @param array $data The data to get a context provider for.
+ * @return \Cake\View\Form\ContextInterface Context provider.
+ * @throws \RuntimeException When a context instance cannot be generated for given entity.
+ */
+ public function get(ServerRequest $request, array $data = []): ContextInterface
+ {
+ $data += ['entity' => null];
+
+ foreach ($this->providers as $provider) {
+ $check = $provider['callable'];
+ $context = $check($request, $data);
+ if ($context) {
+ break;
+ }
+ }
+
+ if (!isset($context)) {
+ throw new RuntimeException(sprintf(
+ 'No context provider found for value of type `%s`.'
+ . ' Use `null` as 1st argument of FormHelper::create() to create a context-less form.',
+ getTypeName($data['entity'])
+ ));
+ }
+
+ return $context;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Form/ContextInterface.php b/app/vendor/cakephp/cakephp/src/View/Form/ContextInterface.php
new file mode 100644
index 000000000..0984c9d35
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Form/ContextInterface.php
@@ -0,0 +1,136 @@
+validators when
+ * dealing with associated forms.
+ */
+class EntityContext implements ContextInterface
+{
+ use LocatorAwareTrait;
+
+ /**
+ * Context data for this object.
+ *
+ * @var array
+ */
+ protected $_context;
+
+ /**
+ * The name of the top level entity/table object.
+ *
+ * @var string
+ */
+ protected $_rootName;
+
+ /**
+ * Boolean to track whether or not the entity is a
+ * collection.
+ *
+ * @var bool
+ */
+ protected $_isCollection = false;
+
+ /**
+ * A dictionary of tables
+ *
+ * @var \Cake\ORM\Table[]
+ */
+ protected $_tables = [];
+
+ /**
+ * Dictionary of validators.
+ *
+ * @var \Cake\Validation\Validator[]
+ */
+ protected $_validator = [];
+
+ /**
+ * Constructor.
+ *
+ * @param array $context Context info.
+ */
+ public function __construct(array $context)
+ {
+ $context += [
+ 'entity' => null,
+ 'table' => null,
+ 'validator' => [],
+ ];
+ $this->_context = $context;
+ $this->_prepare();
+ }
+
+ /**
+ * Prepare some additional data from the context.
+ *
+ * If the table option was provided to the constructor and it
+ * was a string, TableLocator will be used to get the correct table instance.
+ *
+ * If an object is provided as the table option, it will be used as is.
+ *
+ * If no table option is provided, the table name will be derived based on
+ * naming conventions. This inference will work with a number of common objects
+ * like arrays, Collection objects and ResultSets.
+ *
+ * @return void
+ * @throws \RuntimeException When a table object cannot be located/inferred.
+ */
+ protected function _prepare(): void
+ {
+ /** @var \Cake\ORM\Table|null $table */
+ $table = $this->_context['table'];
+ /** @var \Cake\Datasource\EntityInterface|iterable $entity */
+ $entity = $this->_context['entity'];
+ if (empty($table)) {
+ if (is_iterable($entity)) {
+ foreach ($entity as $e) {
+ $entity = $e;
+ break;
+ }
+ }
+ $isEntity = $entity instanceof EntityInterface;
+
+ if ($isEntity) {
+ /** @psalm-suppress PossiblyInvalidMethodCall */
+ $table = $entity->getSource();
+ }
+ if (!$table && $isEntity && get_class($entity) !== Entity::class) {
+ [, $entityClass] = namespaceSplit(get_class($entity));
+ $table = Inflector::pluralize($entityClass);
+ }
+ }
+ if (is_string($table) && strlen($table)) {
+ $table = $this->getTableLocator()->get($table);
+ }
+
+ if (!($table instanceof Table)) {
+ throw new RuntimeException(
+ 'Unable to find table class for current entity.'
+ );
+ }
+ $this->_isCollection = (
+ is_array($entity) ||
+ $entity instanceof Traversable
+ );
+
+ $alias = $this->_rootName = $table->getAlias();
+ $this->_tables[$alias] = $table;
+ }
+
+ /**
+ * Get the primary key data for the context.
+ *
+ * Gets the primary key columns from the root entity's schema.
+ *
+ * @return string[]
+ * @deprecated 4.0.0 Renamed to {@link getPrimaryKey()}.
+ */
+ public function primaryKey(): array
+ {
+ deprecationWarning('`EntityContext::primaryKey()` is deprecated. Use `EntityContext::getPrimaryKey()`.');
+
+ return (array)$this->_tables[$this->_rootName]->getPrimaryKey();
+ }
+
+ /**
+ * Get the primary key data for the context.
+ *
+ * Gets the primary key columns from the root entity's schema.
+ *
+ * @return string[]
+ */
+ public function getPrimaryKey(): array
+ {
+ return (array)$this->_tables[$this->_rootName]->getPrimaryKey();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isPrimaryKey(string $field): bool
+ {
+ $parts = explode('.', $field);
+ $table = $this->_getTable($parts);
+ if (!$table) {
+ return false;
+ }
+ $primaryKey = (array)$table->getPrimaryKey();
+
+ return in_array(array_pop($parts), $primaryKey, true);
+ }
+
+ /**
+ * Check whether or not this form is a create or update.
+ *
+ * If the context is for a single entity, the entity's isNew() method will
+ * be used. If isNew() returns null, a create operation will be assumed.
+ *
+ * If the context is for a collection or array the first object in the
+ * collection will be used.
+ *
+ * @return bool
+ */
+ public function isCreate(): bool
+ {
+ $entity = $this->_context['entity'];
+ if (is_iterable($entity)) {
+ foreach ($entity as $e) {
+ $entity = $e;
+ break;
+ }
+ }
+ if ($entity instanceof EntityInterface) {
+ return $entity->isNew() !== false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the value for a given path.
+ *
+ * Traverses the entity data and finds the value for $path.
+ *
+ * @param string $field The dot separated path to the value.
+ * @param array $options Options:
+ *
+ * - `default`: Default value to return if no value found in data or
+ * entity.
+ * - `schemaDefault`: Boolean indicating whether default value from table
+ * schema should be used if it's not explicitly provided.
+ * @return mixed The value of the field or null on a miss.
+ */
+ public function val(string $field, array $options = [])
+ {
+ $options += [
+ 'default' => null,
+ 'schemaDefault' => true,
+ ];
+
+ if (empty($this->_context['entity'])) {
+ return $options['default'];
+ }
+ $parts = explode('.', $field);
+ $entity = $this->entity($parts);
+
+ if ($entity && end($parts) === '_ids') {
+ return $this->_extractMultiple($entity, $parts);
+ }
+
+ if ($entity instanceof EntityInterface) {
+ $part = end($parts);
+
+ if ($entity instanceof InvalidPropertyInterface) {
+ $val = $entity->getInvalidField($part);
+ if ($val !== null) {
+ return $val;
+ }
+ }
+
+ $val = $entity->get($part);
+ if ($val !== null) {
+ return $val;
+ }
+ if (
+ $options['default'] !== null
+ || !$options['schemaDefault']
+ || !$entity->isNew()
+ ) {
+ return $options['default'];
+ }
+
+ return $this->_schemaDefault($parts);
+ }
+ if (is_array($entity) || $entity instanceof ArrayAccess) {
+ $key = array_pop($parts);
+
+ return $entity[$key] ?? $options['default'];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get default value from table schema for given entity field.
+ *
+ * @param string[] $parts Each one of the parts in a path for a field name
+ * @return mixed
+ */
+ protected function _schemaDefault(array $parts)
+ {
+ $table = $this->_getTable($parts);
+ if ($table === null) {
+ return null;
+ }
+ $field = end($parts);
+ $defaults = $table->getSchema()->defaultValues();
+ if (!array_key_exists($field, $defaults)) {
+ return null;
+ }
+
+ return $defaults[$field];
+ }
+
+ /**
+ * Helper method used to extract all the primary key values out of an array, The
+ * primary key column is guessed out of the provided $path array
+ *
+ * @param mixed $values The list from which to extract primary keys from
+ * @param string[] $path Each one of the parts in a path for a field name
+ * @return array|null
+ */
+ protected function _extractMultiple($values, array $path): ?array
+ {
+ if (!is_iterable($values)) {
+ return null;
+ }
+ $table = $this->_getTable($path, false);
+ $primary = $table ? (array)$table->getPrimaryKey() : ['id'];
+
+ return (new Collection($values))->extract($primary[0])->toArray();
+ }
+
+ /**
+ * Fetch the entity or data value for a given path
+ *
+ * This method will traverse the given path and find the entity
+ * or array value for a given path.
+ *
+ * If you only want the terminal Entity for a path use `leafEntity` instead.
+ *
+ * @param array|null $path Each one of the parts in a path for a field name
+ * or null to get the entity passed in constructor context.
+ * @return \Cake\Datasource\EntityInterface|iterable|null
+ * @throws \RuntimeException When properties cannot be read.
+ */
+ public function entity(?array $path = null)
+ {
+ if ($path === null) {
+ return $this->_context['entity'];
+ }
+
+ $oneElement = count($path) === 1;
+ if ($oneElement && $this->_isCollection) {
+ return null;
+ }
+ $entity = $this->_context['entity'];
+ if ($oneElement) {
+ return $entity;
+ }
+
+ if ($path[0] === $this->_rootName) {
+ $path = array_slice($path, 1);
+ }
+
+ $len = count($path);
+ $last = $len - 1;
+ for ($i = 0; $i < $len; $i++) {
+ $prop = $path[$i];
+ $next = $this->_getProp($entity, $prop);
+ $isLast = ($i === $last);
+ if (!$isLast && $next === null && $prop !== '_ids') {
+ $table = $this->_getTable($path);
+ if ($table) {
+ return $table->newEmptyEntity();
+ }
+ }
+
+ $isTraversable = (
+ is_iterable($next) ||
+ $next instanceof EntityInterface
+ );
+ if ($isLast || !$isTraversable) {
+ return $entity;
+ }
+ $entity = $next;
+ }
+ throw new RuntimeException(sprintf(
+ 'Unable to fetch property "%s"',
+ implode('.', $path)
+ ));
+ }
+
+ /**
+ * Fetch the terminal or leaf entity for the given path.
+ *
+ * Traverse the path until an entity cannot be found. Lists containing
+ * entities will be traversed if the first element contains an entity.
+ * Otherwise the containing Entity will be assumed to be the terminal one.
+ *
+ * @param array|null $path Each one of the parts in a path for a field name
+ * or null to get the entity passed in constructor context.
+ * @return array Containing the found entity, and remaining un-matched path.
+ * @throws \RuntimeException When properties cannot be read.
+ */
+ protected function leafEntity($path = null)
+ {
+ if ($path === null) {
+ return $this->_context['entity'];
+ }
+
+ $oneElement = count($path) === 1;
+ if ($oneElement && $this->_isCollection) {
+ throw new RuntimeException(sprintf(
+ 'Unable to fetch property "%s"',
+ implode('.', $path)
+ ));
+ }
+ $entity = $this->_context['entity'];
+ if ($oneElement) {
+ return [$entity, $path];
+ }
+
+ if ($path[0] === $this->_rootName) {
+ $path = array_slice($path, 1);
+ }
+
+ $len = count($path);
+ $leafEntity = $entity;
+ for ($i = 0; $i < $len; $i++) {
+ $prop = $path[$i];
+ $next = $this->_getProp($entity, $prop);
+
+ // Did not dig into an entity, return the current one.
+ if (is_array($entity) && !($next instanceof EntityInterface || $next instanceof Traversable)) {
+ return [$leafEntity, array_slice($path, $i - 1)];
+ }
+
+ if ($next instanceof EntityInterface) {
+ $leafEntity = $next;
+ }
+
+ // If we are at the end of traversable elements
+ // return the last entity found.
+ $isTraversable = (
+ is_array($next) ||
+ $next instanceof Traversable ||
+ $next instanceof EntityInterface
+ );
+ if (!$isTraversable) {
+ return [$leafEntity, array_slice($path, $i)];
+ }
+ $entity = $next;
+ }
+ throw new RuntimeException(sprintf(
+ 'Unable to fetch property "%s"',
+ implode('.', $path)
+ ));
+ }
+
+ /**
+ * Read property values or traverse arrays/iterators.
+ *
+ * @param mixed $target The entity/array/collection to fetch $field from.
+ * @param string $field The next field to fetch.
+ * @return mixed
+ */
+ protected function _getProp($target, $field)
+ {
+ if (is_array($target) && isset($target[$field])) {
+ return $target[$field];
+ }
+ if ($target instanceof EntityInterface) {
+ return $target->get($field);
+ }
+ if ($target instanceof Traversable) {
+ foreach ($target as $i => $val) {
+ if ((string)$i === $field) {
+ return $val;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * Check if a field should be marked as required.
+ *
+ * @param string $field The dot separated path to the field you want to check.
+ * @return bool|null
+ */
+ public function isRequired(string $field): ?bool
+ {
+ $parts = explode('.', $field);
+ $entity = $this->entity($parts);
+
+ $isNew = true;
+ if ($entity instanceof EntityInterface) {
+ $isNew = $entity->isNew();
+ }
+
+ $validator = $this->_getValidator($parts);
+ $fieldName = array_pop($parts);
+ if (!$validator->hasField($fieldName)) {
+ return null;
+ }
+ if ($this->type($field) !== 'boolean') {
+ return !$validator->isEmptyAllowed($fieldName, $isNew);
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRequiredMessage(string $field): ?string
+ {
+ $parts = explode('.', $field);
+
+ $validator = $this->_getValidator($parts);
+ $fieldName = array_pop($parts);
+ if (!$validator->hasField($fieldName)) {
+ return null;
+ }
+
+ $ruleset = $validator->field($fieldName);
+ if (!$ruleset->isEmptyAllowed()) {
+ return $validator->getNotEmptyMessage($fieldName);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get field length from validation
+ *
+ * @param string $field The dot separated path to the field you want to check.
+ * @return int|null
+ */
+ public function getMaxLength(string $field): ?int
+ {
+ $parts = explode('.', $field);
+ $validator = $this->_getValidator($parts);
+ $fieldName = array_pop($parts);
+
+ if ($validator->hasField($fieldName)) {
+ foreach ($validator->field($fieldName)->rules() as $rule) {
+ if ($rule->get('rule') === 'maxLength') {
+ return $rule->get('pass')[0];
+ }
+ }
+ }
+
+ $attributes = $this->attributes($field);
+ if (!empty($attributes['length'])) {
+ return (int)$attributes['length'];
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the field names from the top level entity.
+ *
+ * If the context is for an array of entities, the 0th index will be used.
+ *
+ * @return string[] Array of field names in the table/entity.
+ */
+ public function fieldNames(): array
+ {
+ $table = $this->_getTable('0');
+ if (!$table) {
+ return [];
+ }
+
+ return $table->getSchema()->columns();
+ }
+
+ /**
+ * Get the validator associated to an entity based on naming
+ * conventions.
+ *
+ * @param array $parts Each one of the parts in a path for a field name
+ * @return \Cake\Validation\Validator
+ * @throws \RuntimeException If validator cannot be retrieved based on the parts.
+ */
+ protected function _getValidator(array $parts): Validator
+ {
+ $keyParts = array_filter(array_slice($parts, 0, -1), function ($part) {
+ return !is_numeric($part);
+ });
+ $key = implode('.', $keyParts);
+ $entity = $this->entity($parts) ?: null;
+
+ if (isset($this->_validator[$key])) {
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $this->_validator[$key]->setProvider('entity', $entity);
+
+ return $this->_validator[$key];
+ }
+
+ $table = $this->_getTable($parts);
+ if (!$table) {
+ throw new RuntimeException('Validator not found: ' . $key);
+ }
+ $alias = $table->getAlias();
+
+ $method = 'default';
+ if (is_string($this->_context['validator'])) {
+ $method = $this->_context['validator'];
+ } elseif (isset($this->_context['validator'][$alias])) {
+ $method = $this->_context['validator'][$alias];
+ }
+
+ $validator = $table->getValidator($method);
+ /** @psalm-suppress PossiblyInvalidArgument */
+ $validator->setProvider('entity', $entity);
+
+ return $this->_validator[$key] = $validator;
+ }
+
+ /**
+ * Get the table instance from a property path
+ *
+ * @param string[]|string|\Cake\Datasource\EntityInterface $parts Each one of the parts in a path for a field name
+ * @param bool $fallback Whether or not to fallback to the last found table
+ * when a nonexistent field/property is being encountered.
+ * @return \Cake\ORM\Table|null Table instance or null
+ */
+ protected function _getTable($parts, $fallback = true): ?Table
+ {
+ if (!is_array($parts) || count($parts) === 1) {
+ return $this->_tables[$this->_rootName];
+ }
+
+ $normalized = array_slice(array_filter($parts, function ($part) {
+ return !is_numeric($part);
+ }), 0, -1);
+
+ $path = implode('.', $normalized);
+ if (isset($this->_tables[$path])) {
+ return $this->_tables[$path];
+ }
+
+ if (current($normalized) === $this->_rootName) {
+ $normalized = array_slice($normalized, 1);
+ }
+
+ $table = $this->_tables[$this->_rootName];
+ $assoc = null;
+ foreach ($normalized as $part) {
+ if ($part === '_joinData') {
+ if ($assoc !== null) {
+ $table = $assoc->junction();
+ $assoc = null;
+ continue;
+ }
+ } else {
+ $associationCollection = $table->associations();
+ $assoc = $associationCollection->getByProperty($part);
+ }
+
+ if ($assoc === null) {
+ if ($fallback) {
+ break;
+ }
+
+ return null;
+ }
+
+ $table = $assoc->getTarget();
+ }
+
+ return $this->_tables[$path] = $table;
+ }
+
+ /**
+ * Get the abstract field type for a given field name.
+ *
+ * @param string $field A dot separated path to get a schema type for.
+ * @return string|null An abstract data type or null.
+ * @see \Cake\Database\TypeFactory
+ */
+ public function type(string $field): ?string
+ {
+ $parts = explode('.', $field);
+ $table = $this->_getTable($parts);
+ if (!$table) {
+ return null;
+ }
+
+ return $table->getSchema()->baseColumnType(array_pop($parts));
+ }
+
+ /**
+ * Get an associative array of other attributes for a field name.
+ *
+ * @param string $field A dot separated path to get additional data on.
+ * @return array An array of data describing the additional attributes on a field.
+ */
+ public function attributes(string $field): array
+ {
+ $parts = explode('.', $field);
+ $table = $this->_getTable($parts);
+ if (!$table) {
+ return [];
+ }
+
+ return array_intersect_key(
+ (array)$table->getSchema()->getColumn(array_pop($parts)),
+ array_flip(static::VALID_ATTRIBUTES)
+ );
+ }
+
+ /**
+ * Check whether or not a field has an error attached to it
+ *
+ * @param string $field A dot separated path to check errors on.
+ * @return bool Returns true if the errors for the field are not empty.
+ */
+ public function hasError(string $field): bool
+ {
+ return $this->error($field) !== [];
+ }
+
+ /**
+ * Get the errors for a given field
+ *
+ * @param string $field A dot separated path to check errors on.
+ * @return array An array of errors.
+ */
+ public function error(string $field): array
+ {
+ $parts = explode('.', $field);
+ try {
+ [$entity, $remainingParts] = $this->leafEntity($parts);
+ } catch (RuntimeException $e) {
+ return [];
+ }
+ if (count($remainingParts) === 0) {
+ return $entity->getErrors();
+ }
+
+ if ($entity instanceof EntityInterface) {
+ $error = $entity->getError(implode('.', $remainingParts));
+ if ($error) {
+ return $error;
+ }
+
+ return $entity->getError(array_pop($parts));
+ }
+
+ return [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Form/FormContext.php b/app/vendor/cakephp/cakephp/src/View/Form/FormContext.php
new file mode 100644
index 000000000..f0475ec69
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Form/FormContext.php
@@ -0,0 +1,228 @@
+ null,
+ ];
+ $this->_form = $context['entity'];
+ }
+
+ /**
+ * Get the fields used in the context as a primary key.
+ *
+ * @return string[]
+ * @deprecated 4.0.0 Renamed to {@link getPrimaryKey()}.
+ */
+ public function primaryKey(): array
+ {
+ deprecationWarning('`FormContext::primaryKey()` is deprecated. Use `FormContext::getPrimaryKey()`.');
+
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPrimaryKey(): array
+ {
+ return [];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isPrimaryKey(string $field): bool
+ {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isCreate(): bool
+ {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function val(string $field, array $options = [])
+ {
+ $options += [
+ 'default' => null,
+ 'schemaDefault' => true,
+ ];
+
+ $val = $this->_form->getData($field);
+ if ($val !== null) {
+ return $val;
+ }
+
+ if ($options['default'] !== null || !$options['schemaDefault']) {
+ return $options['default'];
+ }
+
+ return $this->_schemaDefault($field);
+ }
+
+ /**
+ * Get default value from form schema for given field.
+ *
+ * @param string $field Field name.
+ * @return mixed
+ */
+ protected function _schemaDefault(string $field)
+ {
+ $field = $this->_form->getSchema()->field($field);
+ if ($field) {
+ return $field['default'];
+ }
+
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isRequired(string $field): ?bool
+ {
+ $validator = $this->_form->getValidator();
+ if (!$validator->hasField($field)) {
+ return null;
+ }
+ if ($this->type($field) !== 'boolean') {
+ return !$validator->isEmptyAllowed($field, $this->isCreate());
+ }
+
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getRequiredMessage(string $field): ?string
+ {
+ $parts = explode('.', $field);
+
+ $validator = $this->_form->getValidator();
+ $fieldName = array_pop($parts);
+ if (!$validator->hasField($fieldName)) {
+ return null;
+ }
+
+ $ruleset = $validator->field($fieldName);
+ if (!$ruleset->isEmptyAllowed()) {
+ return $validator->getNotEmptyMessage($fieldName);
+ }
+
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMaxLength(string $field): ?int
+ {
+ $validator = $this->_form->getValidator();
+ if (!$validator->hasField($field)) {
+ return null;
+ }
+ foreach ($validator->field($field)->rules() as $rule) {
+ if ($rule->get('rule') === 'maxLength') {
+ return $rule->get('pass')[0];
+ }
+ }
+
+ $attributes = $this->attributes($field);
+ if (!empty($attributes['length'])) {
+ return $attributes['length'];
+ }
+
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function fieldNames(): array
+ {
+ return $this->_form->getSchema()->fields();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function type(string $field): ?string
+ {
+ return $this->_form->getSchema()->fieldType($field);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function attributes(string $field): array
+ {
+ return array_intersect_key(
+ (array)$this->_form->getSchema()->field($field),
+ array_flip(static::VALID_ATTRIBUTES)
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasError(string $field): bool
+ {
+ $errors = $this->error($field);
+
+ return count($errors) > 0;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function error(string $field): array
+ {
+ return (array)Hash::get($this->_form->getErrors(), $field, []);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Form/NullContext.php b/app/vendor/cakephp/cakephp/src/View/Form/NullContext.php
new file mode 100644
index 000000000..5a0074684
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Form/NullContext.php
@@ -0,0 +1,144 @@
+_View = $view;
+ $this->setConfig($config);
+
+ if (!empty($this->helpers)) {
+ $this->_helperMap = $view->helpers()->normalizeArray($this->helpers);
+ }
+
+ $this->initialize($config);
+ }
+
+ /**
+ * Provide non fatal errors on missing method calls.
+ *
+ * @param string $method Method to invoke
+ * @param array $params Array of params for the method.
+ * @return mixed|void
+ */
+ public function __call(string $method, array $params)
+ {
+ trigger_error(sprintf('Method %1$s::%2$s does not exist', static::class, $method), E_USER_WARNING);
+ }
+
+ /**
+ * Lazy loads helpers.
+ *
+ * @param string $name Name of the property being accessed.
+ * @return \Cake\View\Helper|null|void Helper instance if helper with provided name exists
+ */
+ public function __get(string $name)
+ {
+ if (isset($this->_helperMap[$name]) && !isset($this->{$name})) {
+ $config = ['enabled' => false] + (array)$this->_helperMap[$name]['config'];
+ $this->{$name} = $this->_View->loadHelper($this->_helperMap[$name]['class'], $config);
+
+ return $this->{$name};
+ }
+ }
+
+ /**
+ * Get the view instance this helper is bound to.
+ *
+ * @return \Cake\View\View The bound view instance.
+ */
+ public function getView(): View
+ {
+ return $this->_View;
+ }
+
+ /**
+ * Returns a string to be used as onclick handler for confirm dialogs.
+ *
+ * @param string $okCode Code to be executed after user chose 'OK'
+ * @param string $cancelCode Code to be executed after user chose 'Cancel'
+ * @return string "onclick" JS code
+ */
+ protected function _confirm(string $okCode, string $cancelCode): string
+ {
+ return "if (confirm(this.dataset.confirmMessage)) { {$okCode} } {$cancelCode}";
+ }
+
+ /**
+ * Adds the given class to the element options
+ *
+ * @param array $options Array options/attributes to add a class to
+ * @param string $class The class name being added.
+ * @param string $key the key to use for class. Defaults to `'class'`.
+ * @return array Array of options with $key set.
+ */
+ public function addClass(array $options, string $class, string $key = 'class'): array
+ {
+ if (isset($options[$key]) && is_array($options[$key])) {
+ $options[$key][] = $class;
+ } elseif (isset($options[$key]) && trim($options[$key])) {
+ $options[$key] .= ' ' . $class;
+ } else {
+ $options[$key] = $class;
+ }
+
+ return $options;
+ }
+
+ /**
+ * Get the View callbacks this helper is interested in.
+ *
+ * By defining one of the callback methods a helper is assumed
+ * to be interested in the related event.
+ *
+ * Override this method if you need to add non-conventional event listeners.
+ * Or if you want helpers to listen to non-standard events.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ $eventMap = [
+ 'View.beforeRenderFile' => 'beforeRenderFile',
+ 'View.afterRenderFile' => 'afterRenderFile',
+ 'View.beforeRender' => 'beforeRender',
+ 'View.afterRender' => 'afterRender',
+ 'View.beforeLayout' => 'beforeLayout',
+ 'View.afterLayout' => 'afterLayout',
+ ];
+ $events = [];
+ foreach ($eventMap as $event => $method) {
+ if (method_exists($this, $method)) {
+ $events[$event] = $method;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Constructor hook method.
+ *
+ * Implement this method to avoid having to overwrite the constructor and call parent.
+ *
+ * @param array $config The configuration settings provided to this helper.
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ }
+
+ /**
+ * Returns an array that can be used to describe the internal state of this
+ * object.
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'helpers' => $this->helpers,
+ 'implementedEvents' => $this->implementedEvents(),
+ '_config' => $this->getConfig(),
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/BreadcrumbsHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/BreadcrumbsHelper.php
new file mode 100644
index 000000000..11887ead5
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Helper/BreadcrumbsHelper.php
@@ -0,0 +1,347 @@
+ [
+ 'wrapper' => '{{content}}
',
+ 'item' => '{{title}} {{separator}}',
+ 'itemWithoutLink' => '{{title}} {{separator}}',
+ 'separator' => '{{separator}} ',
+ ],
+ ];
+
+ /**
+ * The crumb list.
+ *
+ * @var array
+ */
+ protected $crumbs = [];
+
+ /**
+ * Add a crumb to the end of the trail.
+ *
+ * @param string|array $title If provided as a string, it represents the title of the crumb.
+ * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a
+ * single crumb. Arrays are expected to be of this form:
+ *
+ * - *title* The title of the crumb
+ * - *link* The link of the crumb. If not provided, no link will be made
+ * - *options* Options of the crumb. See description of params option of this method.
+ *
+ * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+ * Url::build() or null / empty if the crumb does not have a link.
+ * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+ * be rendered in (a tag by default). It accepts two special keys:
+ *
+ * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+ * the link)
+ * - *templateVars*: Specific template vars in case you override the templates provided.
+ * @return $this
+ */
+ public function add($title, $url = null, array $options = [])
+ {
+ if (is_array($title)) {
+ foreach ($title as $crumb) {
+ $this->crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []];
+ }
+
+ return $this;
+ }
+
+ $this->crumbs[] = compact('title', 'url', 'options');
+
+ return $this;
+ }
+
+ /**
+ * Prepend a crumb to the start of the queue.
+ *
+ * @param string|array $title If provided as a string, it represents the title of the crumb.
+ * Alternatively, if you want to add multiple crumbs at once, you can provide an array, with each values being a
+ * single crumb. Arrays are expected to be of this form:
+ *
+ * - *title* The title of the crumb
+ * - *link* The link of the crumb. If not provided, no link will be made
+ * - *options* Options of the crumb. See description of params option of this method.
+ *
+ * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+ * Url::build() or null / empty if the crumb does not have a link.
+ * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+ * be rendered in (a tag by default). It accepts two special keys:
+ *
+ * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+ * the link)
+ * - *templateVars*: Specific template vars in case you override the templates provided.
+ * @return $this
+ */
+ public function prepend($title, $url = null, array $options = [])
+ {
+ if (is_array($title)) {
+ $crumbs = [];
+ foreach ($title as $crumb) {
+ $crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => []];
+ }
+
+ array_splice($this->crumbs, 0, 0, $crumbs);
+
+ return $this;
+ }
+
+ array_unshift($this->crumbs, compact('title', 'url', 'options'));
+
+ return $this;
+ }
+
+ /**
+ * Insert a crumb at a specific index.
+ *
+ * If the index already exists, the new crumb will be inserted,
+ * and the existing element will be shifted one index greater.
+ * If the index is out of bounds, it will throw an exception.
+ *
+ * @param int $index The index to insert at.
+ * @param string $title Title of the crumb.
+ * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+ * Url::build() or null / empty if the crumb does not have a link.
+ * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+ * be rendered in (a tag by default). It accepts two special keys:
+ *
+ * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+ * the link)
+ * - *templateVars*: Specific template vars in case you override the templates provided.
+ * @return $this
+ * @throws \LogicException In case the index is out of bound
+ */
+ public function insertAt(int $index, string $title, $url = null, array $options = [])
+ {
+ if (!isset($this->crumbs[$index])) {
+ throw new LogicException(sprintf("No crumb could be found at index '%s'", $index));
+ }
+
+ array_splice($this->crumbs, $index, 0, [compact('title', 'url', 'options')]);
+
+ return $this;
+ }
+
+ /**
+ * Insert a crumb before the first matching crumb with the specified title.
+ *
+ * Finds the index of the first crumb that matches the provided class,
+ * and inserts the supplied callable before it.
+ *
+ * @param string $matchingTitle The title of the crumb you want to insert this one before.
+ * @param string $title Title of the crumb.
+ * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+ * Url::build() or null / empty if the crumb does not have a link.
+ * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+ * be rendered in (a tag by default). It accepts two special keys:
+ *
+ * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+ * the link)
+ * - *templateVars*: Specific template vars in case you override the templates provided.
+ * @return $this
+ * @throws \LogicException In case the matching crumb can not be found
+ */
+ public function insertBefore(string $matchingTitle, string $title, $url = null, array $options = [])
+ {
+ $key = $this->findCrumb($matchingTitle);
+
+ if ($key === null) {
+ throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle));
+ }
+
+ return $this->insertAt($key, $title, $url, $options);
+ }
+
+ /**
+ * Insert a crumb after the first matching crumb with the specified title.
+ *
+ * Finds the index of the first crumb that matches the provided class,
+ * and inserts the supplied callable before it.
+ *
+ * @param string $matchingTitle The title of the crumb you want to insert this one after.
+ * @param string $title Title of the crumb.
+ * @param string|array|null $url URL of the crumb. Either a string, an array of route params to pass to
+ * Url::build() or null / empty if the crumb does not have a link.
+ * @param array $options Array of options. These options will be used as attributes HTML attribute the crumb will
+ * be rendered in (a tag by default). It accepts two special keys:
+ *
+ * - *innerAttrs*: An array that allows you to define attributes for the inner element of the crumb (by default, to
+ * the link)
+ * - *templateVars*: Specific template vars in case you override the templates provided.
+ * @return $this
+ * @throws \LogicException In case the matching crumb can not be found.
+ */
+ public function insertAfter(string $matchingTitle, string $title, $url = null, array $options = [])
+ {
+ $key = $this->findCrumb($matchingTitle);
+
+ if ($key === null) {
+ throw new LogicException(sprintf("No crumb matching '%s' could be found.", $matchingTitle));
+ }
+
+ return $this->insertAt($key + 1, $title, $url, $options);
+ }
+
+ /**
+ * Returns the crumb list.
+ *
+ * @return array
+ */
+ public function getCrumbs(): array
+ {
+ return $this->crumbs;
+ }
+
+ /**
+ * Removes all existing crumbs.
+ *
+ * @return $this
+ */
+ public function reset()
+ {
+ $this->crumbs = [];
+
+ return $this;
+ }
+
+ /**
+ * Renders the breadcrumbs trail.
+ *
+ * @param array $attributes Array of attributes applied to the `wrapper` template. Accepts the `templateVars` key to
+ * allow the insertion of custom template variable in the template.
+ * @param array $separator Array of attributes for the `separator` template.
+ * Possible properties are :
+ *
+ * - *separator* The string to be displayed as a separator
+ * - *templateVars* Allows the insertion of custom template variable in the template
+ * - *innerAttrs* To provide attributes in case your separator is divided in two elements.
+ *
+ * All other properties will be converted as HTML attributes and will replace the *attrs* key in the template.
+ * If you use the default for this option (empty), it will not render a separator.
+ * @return string The breadcrumbs trail
+ */
+ public function render(array $attributes = [], array $separator = []): string
+ {
+ if (!$this->crumbs) {
+ return '';
+ }
+
+ $crumbs = $this->crumbs;
+ $crumbsCount = count($crumbs);
+ $templater = $this->templater();
+ $separatorString = '';
+
+ if ($separator) {
+ if (isset($separator['innerAttrs'])) {
+ $separator['innerAttrs'] = $templater->formatAttributes($separator['innerAttrs']);
+ }
+
+ $separator['attrs'] = $templater->formatAttributes(
+ $separator,
+ ['innerAttrs', 'separator']
+ );
+
+ $separatorString = $this->formatTemplate('separator', $separator);
+ }
+
+ $crumbTrail = '';
+ foreach ($crumbs as $key => $crumb) {
+ $url = $crumb['url'] ? $this->Url->build($crumb['url']) : null;
+ $title = $crumb['title'];
+ $options = $crumb['options'];
+
+ $optionsLink = [];
+ if (isset($options['innerAttrs'])) {
+ $optionsLink = $options['innerAttrs'];
+ unset($options['innerAttrs']);
+ }
+
+ $template = 'item';
+ $templateParams = [
+ 'attrs' => $templater->formatAttributes($options, ['templateVars']),
+ 'innerAttrs' => $templater->formatAttributes($optionsLink),
+ 'title' => $title,
+ 'url' => $url,
+ 'separator' => '',
+ 'templateVars' => $options['templateVars'] ?? [],
+ ];
+
+ if (!$url) {
+ $template = 'itemWithoutLink';
+ }
+
+ if ($separatorString && $key !== $crumbsCount - 1) {
+ $templateParams['separator'] = $separatorString;
+ }
+
+ $crumbTrail .= $this->formatTemplate($template, $templateParams);
+ }
+
+ $crumbTrail = $this->formatTemplate('wrapper', [
+ 'content' => $crumbTrail,
+ 'attrs' => $templater->formatAttributes($attributes, ['templateVars']),
+ 'templateVars' => $attributes['templateVars'] ?? [],
+ ]);
+
+ return $crumbTrail;
+ }
+
+ /**
+ * Search a crumb in the current stack which title matches the one provided as argument.
+ * If found, the index of the matching crumb will be returned.
+ *
+ * @param string $title Title to find.
+ * @return int|null Index of the crumb found, or null if it can not be found.
+ */
+ protected function findCrumb(string $title): ?int
+ {
+ foreach ($this->crumbs as $key => $crumb) {
+ if ($crumb['title'] === $title) {
+ return $key;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/FlashHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/FlashHelper.php
new file mode 100644
index 000000000..78e2f252e
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Helper/FlashHelper.php
@@ -0,0 +1,95 @@
+Flash->render('somekey');
+ * Will default to flash if no param is passed
+ *
+ * You can pass additional information into the flash message generation. This allows you
+ * to consolidate all the parameters for a given type of flash message into the view.
+ *
+ * ```
+ * echo $this->Flash->render('flash', ['params' => ['name' => $user['User']['name']]]);
+ * ```
+ *
+ * This would pass the current user's name into the flash message, so you could create personalized
+ * messages without the controller needing access to that data.
+ *
+ * Lastly you can choose the element that is used for rendering the flash message. Using
+ * custom elements allows you to fully customize how flash messages are generated.
+ *
+ * ```
+ * echo $this->Flash->render('flash', ['element' => 'my_custom_element']);
+ * ```
+ *
+ * If you want to use an element from a plugin for rendering your flash message
+ * you can use the dot notation for the plugin's element name:
+ *
+ * ```
+ * echo $this->Flash->render('flash', [
+ * 'element' => 'MyPlugin.my_custom_element',
+ * ]);
+ * ```
+ *
+ * If you have several messages stored in the Session, each message will be rendered in its own
+ * element.
+ *
+ * @param string $key The [Flash.]key you are rendering in the view.
+ * @param array $options Additional options to use for the creation of this flash message.
+ * Supports the 'params', and 'element' keys that are used in the helper.
+ * @return string|null Rendered flash message or null if flash key does not exist
+ * in session.
+ */
+ public function render(string $key = 'flash', array $options = []): ?string
+ {
+ $messages = $this->_View->getRequest()->getFlash()->consume($key);
+ if ($messages === null) {
+ return null;
+ }
+
+ $out = '';
+ foreach ($messages as $message) {
+ $message = $options + $message;
+ $out .= $this->_View->element($message['element'], $message);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Event listeners.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ return [];
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php b/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php
new file mode 100644
index 000000000..322e9a930
--- /dev/null
+++ b/app/vendor/cakephp/cakephp/src/View/Helper/FormHelper.php
@@ -0,0 +1,2574 @@
+ null,
+ 'errorClass' => 'form-error',
+ 'typeMap' => [
+ 'string' => 'text',
+ 'text' => 'textarea',
+ 'uuid' => 'string',
+ 'datetime' => 'datetime',
+ 'datetimefractional' => 'datetime',
+ 'timestamp' => 'datetime',
+ 'timestampfractional' => 'datetime',
+ 'timestamptimezone' => 'datetime',
+ 'date' => 'date',
+ 'time' => 'time',
+ 'year' => 'year',
+ 'boolean' => 'checkbox',
+ 'float' => 'number',
+ 'integer' => 'number',
+ 'tinyinteger' => 'number',
+ 'smallinteger' => 'number',
+ 'decimal' => 'number',
+ 'binary' => 'file',
+ ],
+ 'templates' => [
+ // Used for button elements in button().
+ 'button' => '',
+ // Used for checkboxes in checkbox() and multiCheckbox().
+ 'checkbox' => '',
+ // Input group wrapper for checkboxes created via control().
+ 'checkboxFormGroup' => '{{label}}',
+ // Wrapper container for checkboxes.
+ 'checkboxWrapper' => '{{label}}',
+ // Error message wrapper elements.
+ 'error' => '',
+ // Container for error items.
+ 'errorList' => '{{content}}
',
+ // Error item wrapper.
+ 'errorItem' => ' {{text}} ',
+ // File input used by file().
+ 'file' => '',
+ // Fieldset element used by allControls().
+ 'fieldset' => '',
+ // Open tag used by create().
+ 'formStart' => '',
+ // General grouping container for control(). Defines input/label ordering.
+ 'formGroup' => '{{label}}{{input}}',
+ // Wrapper content used to hide other content.
+ 'hiddenBlock' => '',
+ // Generic input element.
+ 'input' => '',
+ // Submit input element.
+ 'inputSubmit' => '',
+ // Container element used by control().
+ 'inputContainer' => '{{content}}',
+ // Container element used by control() when a field has an error.
+ 'inputContainerError' => '{{content}}{{error}}',
+ // Label element when inputs are not nested inside the label.
+ 'label' => '',
+ // Label element used for radio and multi-checkbox inputs.
+ 'nestingLabel' => '{{hidden}}',
+ // Legends created by allControls()
+ 'legend' => '',
+ // Multi-Checkbox input set title element.
+ 'multicheckboxTitle' => '',
+ // Multi-Checkbox wrapping container.
+ 'multicheckboxWrapper' => '',
+ // Option element used in select pickers.
+ 'option' => '',
+ // Option group element used in select pickers.
+ 'optgroup' => '',
+ // Select element,
+ 'select' => '',
+ // Multi-select element,
+ 'selectMultiple' => '',
+ // Radio input element,
+ 'radio' => '',
+ // Wrapping container for radio input/label,
+ 'radioWrapper' => '{{label}}',
+ // Textarea input element,
+ 'textarea' => '',
+ // Container for submit buttons.
+ 'submitContainer' => '{{content}}',
+ // Confirm javascript template for postLink()
+ 'confirmJs' => '{{confirm}}',
+ // selected class
+ 'selectedClass' => 'selected',
+ ],
+ // set HTML5 validation message to custom required/empty messages
+ 'autoSetCustomValidity' => true,
+ ];
+
+ /**
+ * Default widgets
+ *
+ * @var array
+ */
+ protected $_defaultWidgets = [
+ 'button' => ['Button'],
+ 'checkbox' => ['Checkbox'],
+ 'file' => ['File'],
+ 'label' => ['Label'],
+ 'nestingLabel' => ['NestingLabel'],
+ 'multicheckbox' => ['MultiCheckbox', 'nestingLabel'],
+ 'radio' => ['Radio', 'nestingLabel'],
+ 'select' => ['SelectBox'],
+ 'textarea' => ['Textarea'],
+ 'datetime' => ['DateTime', 'select'],
+ 'year' => ['Year', 'select'],
+ '_default' => ['Basic'],
+ ];
+
+ /**
+ * Constant used internally to skip the securing process,
+ * and neither add the field to the hash or to the unlocked fields.
+ *
+ * @var string
+ */
+ public const SECURE_SKIP = 'skip';
+
+ /**
+ * Defines the type of form being created. Set by FormHelper::create().
+ *
+ * @var string|null
+ */
+ public $requestType;
+
+ /**
+ * Locator for input widgets.
+ *
+ * @var \Cake\View\Widget\WidgetLocator
+ */
+ protected $_locator;
+
+ /**
+ * Context for the current form.
+ *
+ * @var \Cake\View\Form\ContextInterface|null
+ */
+ protected $_context;
+
+ /**
+ * Context factory.
+ *
+ * @var \Cake\View\Form\ContextFactory|null
+ */
+ protected $_contextFactory;
+
+ /**
+ * The action attribute value of the last created form.
+ * Used to make form/request specific hashes for form tampering protection.
+ *
+ * @var string
+ */
+ protected $_lastAction = '';
+
+ /**
+ * The supported sources that can be used to populate input values.
+ *
+ * `context` - Corresponds to `ContextInterface` instances.
+ * `data` - Corresponds to request data (POST/PUT).
+ * `query` - Corresponds to request's query string.
+ *
+ * @var string[]
+ */
+ protected $supportedValueSources = ['context', 'data', 'query'];
+
+ /**
+ * The default sources.
+ *
+ * @see FormHelper::$supportedValueSources for valid values.
+ * @var string[]
+ */
+ protected $_valueSources = ['data', 'context'];
+
+ /**
+ * Grouped input types.
+ *
+ * @var string[]
+ */
+ protected $_groupedInputTypes = ['radio', 'multicheckbox'];
+
+ /**
+ * Form protector
+ *
+ * @var \Cake\Form\FormProtector|null
+ */
+ protected $formProtector;
+
+ /**
+ * Construct the widgets and binds the default context providers
+ *
+ * @param \Cake\View\View $view The View this helper is being attached to.
+ * @param array $config Configuration settings for the helper.
+ */
+ public function __construct(View $view, array $config = [])
+ {
+ $locator = null;
+ $widgets = $this->_defaultWidgets;
+ if (isset($config['locator'])) {
+ $locator = $config['locator'];
+ unset($config['locator']);
+ }
+ if (isset($config['widgets'])) {
+ if (is_string($config['widgets'])) {
+ $config['widgets'] = (array)$config['widgets'];
+ }
+ $widgets = $config['widgets'] + $widgets;
+ unset($config['widgets']);
+ }
+
+ if (isset($config['groupedInputTypes'])) {
+ $this->_groupedInputTypes = $config['groupedInputTypes'];
+ unset($config['groupedInputTypes']);
+ }
+
+ parent::__construct($view, $config);
+
+ if (!$locator) {
+ $locator = new WidgetLocator($this->templater(), $this->_View, $widgets);
+ }
+ $this->setWidgetLocator($locator);
+ $this->_idPrefix = $this->getConfig('idPrefix');
+ }
+
+ /**
+ * Get the widget locator currently used by the helper.
+ *
+ * @return \Cake\View\Widget\WidgetLocator Current locator instance
+ * @since 3.6.0
+ */
+ public function getWidgetLocator(): WidgetLocator
+ {
+ return $this->_locator;
+ }
+
+ /**
+ * Set the widget locator the helper will use.
+ *
+ * @param \Cake\View\Widget\WidgetLocator $instance The locator instance to set.
+ * @return $this
+ * @since 3.6.0
+ */
+ public function setWidgetLocator(WidgetLocator $instance)
+ {
+ $this->_locator = $instance;
+
+ return $this;
+ }
+
+ /**
+ * Set the context factory the helper will use.
+ *
+ * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set.
+ * @param array $contexts An array of context providers.
+ * @return \Cake\View\Form\ContextFactory
+ */
+ public function contextFactory(?ContextFactory $instance = null, array $contexts = []): ContextFactory
+ {
+ if ($instance === null) {
+ if ($this->_contextFactory === null) {
+ $this->_contextFactory = ContextFactory::createWithDefaults($contexts);
+ }
+
+ return $this->_contextFactory;
+ }
+ $this->_contextFactory = $instance;
+
+ return $this->_contextFactory;
+ }
+
+ /**
+ * Returns an HTML form element.
+ *
+ * ### Options:
+ *
+ * - `type` Form method defaults to autodetecting based on the form context. If
+ * the form context's isCreate() method returns false, a PUT request will be done.
+ * - `method` Set the form's method attribute explicitly.
+ * - `url` The URL the form submits to. Can be a string or a URL array.
+ * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')`
+ * - `enctype` Set the form encoding explicitly. By default `type => file` will set `enctype`
+ * to `multipart/form-data`.
+ * - `templates` The templates you want to use for this form. Any templates will be merged on top of
+ * the already loaded templates. This option can either be a filename in /config that contains
+ * the templates you want to load, or an array of templates to use.
+ * - `context` Additional options for the context class. For example the EntityContext accepts a 'table'
+ * option that allows you to set the specific Table class the form should be based on.
+ * - `idPrefix` Prefix for generated ID attributes.
+ * - `valueSources` The sources that values should be read from. See FormHelper::setValueSources()
+ * - `templateVars` Provide template variables for the formStart template.
+ *
+ * @param mixed $context The context for which the form is being defined.
+ * Can be a ContextInterface instance, ORM entity, ORM resultset, or an
+ * array of meta data. You can use `null` to make a context-less form.
+ * @param array $options An array of html attributes and options.
+ * @return string An formatted opening FORM tag.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create
+ */
+ public function create($context = null, array $options = []): string
+ {
+ $append = '';
+
+ if ($context instanceof ContextInterface) {
+ $this->context($context);
+ } else {
+ if (empty($options['context'])) {
+ $options['context'] = [];
+ }
+ $options['context']['entity'] = $context;
+ $context = $this->_getContext($options['context']);
+ unset($options['context']);
+ }
+
+ $isCreate = $context->isCreate();
+
+ $options += [
+ 'type' => $isCreate ? 'post' : 'put',
+ 'url' => null,
+ 'encoding' => strtolower(Configure::read('App.encoding')),
+ 'templates' => null,
+ 'idPrefix' => null,
+ 'valueSources' => null,
+ ];
+
+ if (isset($options['valueSources'])) {
+ $this->setValueSources($options['valueSources']);
+ unset($options['valueSources']);
+ }
+
+ if ($options['idPrefix'] !== null) {
+ $this->_idPrefix = $options['idPrefix'];
+ }
+ $templater = $this->templater();
+
+ if (!empty($options['templates'])) {
+ $templater->push();
+ $method = is_string($options['templates']) ? 'load' : 'add';
+ $templater->{$method}($options['templates']);
+ }
+ unset($options['templates']);
+
+ if ($options['url'] === false) {
+ $url = $this->_View->getRequest()->getRequestTarget();
+ $action = null;
+ } else {
+ $url = $this->_formUrl($context, $options);
+ $action = $this->Url->build($url);
+ }
+
+ $this->_lastAction($url);
+ unset($options['url'], $options['idPrefix']);
+
+ $htmlAttributes = [];
+ switch (strtolower($options['type'])) {
+ case 'get':
+ $htmlAttributes['method'] = 'get';
+ break;
+ // Set enctype for form
+ case 'file':
+ $htmlAttributes['enctype'] = 'multipart/form-data';
+ $options['type'] = $isCreate ? 'post' : 'put';
+ // Move on
+ case 'put':
+ // Move on
+ case 'delete':
+ // Set patch method
+ case 'patch':
+ $append .= $this->hidden('_method', [
+ 'name' => '_method',
+ 'value' => strtoupper($options['type']),
+ 'secure' => static::SECURE_SKIP,
+ ]);
+ // Default to post method
+ default:
+ $htmlAttributes['method'] = 'post';
+ }
+ if (isset($options['method'])) {
+ $htmlAttributes['method'] = strtolower($options['method']);
+ }
+ if (isset($options['enctype'])) {
+ $htmlAttributes['enctype'] = strtolower($options['enctype']);
+ }
+
+ $this->requestType = strtolower($options['type']);
+
+ if (!empty($options['encoding'])) {
+ $htmlAttributes['accept-charset'] = $options['encoding'];
+ }
+ unset($options['type'], $options['encoding']);
+
+ $htmlAttributes += $options;
+
+ if ($this->requestType !== 'get') {
+ $formTokenData = $this->_View->getRequest()->getAttribute('formTokenData');
+ if ($formTokenData !== null) {
+ $this->formProtector = $this->createFormProtector($formTokenData);
+ }
+
+ $append .= $this->_csrfField();
+ }
+
+ if (!empty($append)) {
+ $append = $templater->format('hiddenBlock', ['content' => $append]);
+ }
+
+ $actionAttr = $templater->formatAttributes(['action' => $action, 'escape' => false]);
+
+ return $this->formatTemplate('formStart', [
+ 'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr,
+ 'templateVars' => $options['templateVars'] ?? [],
+ ]) . $append;
+ }
+
+ /**
+ * Create the URL for a form based on the options.
+ *
+ * @param \Cake\View\Form\ContextInterface $context The context object to use.
+ * @param array $options An array of options from create()
+ * @return string|array The action attribute for the form.
+ */
+ protected function _formUrl(ContextInterface $context, array $options)
+ {
+ $request = $this->_View->getRequest();
+
+ if ($options['url'] === null) {
+ return $request->getRequestTarget();
+ }
+
+ if (
+ is_string($options['url']) ||
+ (is_array($options['url']) &&
+ isset($options['url']['_name']))
+ ) {
+ return $options['url'];
+ }
+
+ $actionDefaults = [
+ 'plugin' => $this->_View->getPlugin(),
+ 'controller' => $request->getParam('controller'),
+ 'action' => $request->getParam('action'),
+ ];
+
+ $action = (array)$options['url'] + $actionDefaults;
+
+ return $action;
+ }
+
+ /**
+ * Correctly store the last created form action URL.
+ *
+ * @param string|array|null $url The URL of the last form.
+ * @return void
+ */
+ protected function _lastAction($url = null): void
+ {
+ $action = Router::url($url, true);
+ $query = parse_url($action, PHP_URL_QUERY);
+ $query = $query ? '?' . $query : '';
+
+ $path = parse_url($action, PHP_URL_PATH) ?: '';
+ $this->_lastAction = $path . $query;
+ }
+
+ /**
+ * Return a CSRF input if the request data is present.
+ * Used to secure forms in conjunction with CsrfMiddleware.
+ *
+ * @return string
+ */
+ protected function _csrfField(): string
+ {
+ $request = $this->_View->getRequest();
+
+ $csrfToken = $request->getAttribute('csrfToken');
+ if (!$csrfToken) {
+ return '';
+ }
+
+ return $this->hidden('_csrfToken', [
+ 'value' => $csrfToken,
+ 'secure' => static::SECURE_SKIP,
+ 'autocomplete' => 'off',
+ ]);
+ }
+
+ /**
+ * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden
+ * input fields where appropriate.
+ *
+ * Resets some parts of the state, shared among multiple FormHelper::create() calls, to defaults.
+ *
+ * @param array $secureAttributes Secure attributes which will be passed as HTML attributes
+ * into the hidden input elements generated for the Security Component.
+ * @return string A closing FORM tag.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#closing-the-form
+ */
+ public function end(array $secureAttributes = []): string
+ {
+ $out = '';
+
+ if ($this->requestType !== 'get' && $this->_View->getRequest()->getAttribute('formTokenData') !== null) {
+ $out .= $this->secure([], $secureAttributes);
+ }
+ $out .= $this->formatTemplate('formEnd', []);
+
+ $this->templater()->pop();
+ $this->requestType = null;
+ $this->_context = null;
+ $this->_valueSources = ['data', 'context'];
+ $this->_idPrefix = $this->getConfig('idPrefix');
+ $this->formProtector = null;
+
+ return $out;
+ }
+
+ /**
+ * Generates a hidden field with a security hash based on the fields used in
+ * the form.
+ *
+ * If $secureAttributes is set, these HTML attributes will be merged into
+ * the hidden input tags generated for the Security Component. This is
+ * especially useful to set HTML5 attributes like 'form'.
+ *
+ * @param array $fields If set specifies the list of fields to be added to
+ * FormProtector for generating the hash.
+ * @param array $secureAttributes will be passed as HTML attributes into the hidden
+ * input elements generated for the Security Component.
+ * @return string A hidden input field with a security hash, or empty string when
+ * secured forms are not in use.
+ */
+ public function secure(array $fields = [], array $secureAttributes = []): string
+ {
+ if (!$this->formProtector) {
+ return '';
+ }
+
+ foreach ($fields as $field => $value) {
+ if (is_int($field)) {
+ $field = $value;
+ $value = null;
+ }
+ $this->formProtector->addField($field, true, $value);
+ }
+
+ $debugSecurity = (bool)Configure::read('debug');
+ if (isset($secureAttributes['debugSecurity'])) {
+ $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity'];
+ unset($secureAttributes['debugSecurity']);
+ }
+ $secureAttributes['secure'] = static::SECURE_SKIP;
+ $secureAttributes['autocomplete'] = 'off';
+
+ $tokenData = $this->formProtector->buildTokenData(
+ $this->_lastAction,
+ $this->_View->getRequest()->getSession()->id()
+ );
+ $tokenFields = array_merge($secureAttributes, [
+ 'value' => $tokenData['fields'],
+ ]);
+ $out = $this->hidden('_Token.fields', $tokenFields);
+ $tokenUnlocked = array_merge($secureAttributes, [
+ 'value' => $tokenData['unlocked'],
+ ]);
+ $out .= $this->hidden('_Token.unlocked', $tokenUnlocked);
+ if ($debugSecurity) {
+ $tokenDebug = array_merge($secureAttributes, [
+ 'value' => $tokenData['debug'],
+ ]);
+ $out .= $this->hidden('_Token.debug', $tokenDebug);
+ }
+
+ return $this->formatTemplate('hiddenBlock', ['content' => $out]);
+ }
+
+ /**
+ * Add to the list of fields that are currently unlocked.
+ *
+ * Unlocked fields are not included in the form protection field hash.
+ *
+ * @param string $name The dot separated name for the field.
+ * @return $this
+ */
+ public function unlockField(string $name)
+ {
+ $this->getFormProtector()->unlockField($name);
+
+ return $this;
+ }
+
+ /**
+ * Create FormProtector instance.
+ *
+ * @param array $formTokenData Token data.
+ * @return \Cake\Form\FormProtector
+ */
+ protected function createFormProtector(array $formTokenData): FormProtector
+ {
+ $session = $this->_View->getRequest()->getSession();
+ $session->start();
+
+ return new FormProtector(
+ $formTokenData
+ );
+ }
+
+ /**
+ * Get form protector instance.
+ *
+ * @return \Cake\Form\FormProtector
+ * @throws \Cake\Core\Exception\CakeException
+ */
+ public function getFormProtector(): FormProtector
+ {
+ if ($this->formProtector === null) {
+ throw new CakeException(
+ '`FormProtector` instance has not been created. Ensure you have loaded the `FormProtectionComponent`'
+ . ' in your controller and called `FormHelper::create()` before calling `FormHelper::unlockField()`.'
+ );
+ }
+
+ return $this->formProtector;
+ }
+
+ /**
+ * Returns true if there is an error for the given field, otherwise false
+ *
+ * @param string $field This should be "modelname.fieldname"
+ * @return bool If there are errors this method returns true, else false.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#displaying-and-checking-errors
+ */
+ public function isFieldError(string $field): bool
+ {
+ return $this->_getContext()->hasError($field);
+ }
+
+ /**
+ * Returns a formatted error message for given form field, '' if no errors.
+ *
+ * Uses the `error`, `errorList` and `errorItem` templates. The `errorList` and
+ * `errorItem` templates are used to format multiple error messages per field.
+ *
+ * ### Options:
+ *
+ * - `escape` boolean - Whether or not to html escape the contents of the error.
+ *
+ * @param string $field A field name, like "modelname.fieldname"
+ * @param string|array|null $text Error message as string or array of messages. If an array,
+ * it should be a hash of key names => messages.
+ * @param array $options See above.
+ * @return string Formatted errors or ''.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#displaying-and-checking-errors
+ */
+ public function error(string $field, $text = null, array $options = []): string
+ {
+ if (substr($field, -5) === '._ids') {
+ $field = substr($field, 0, -5);
+ }
+ $options += ['escape' => true];
+
+ $context = $this->_getContext();
+ if (!$context->hasError($field)) {
+ return '';
+ }
+ $error = $context->error($field);
+
+ if (is_array($text)) {
+ $tmp = [];
+ foreach ($error as $k => $e) {
+ if (isset($text[$k])) {
+ $tmp[] = $text[$k];
+ } elseif (isset($text[$e])) {
+ $tmp[] = $text[$e];
+ } else {
+ $tmp[] = $e;
+ }
+ }
+ $text = $tmp;
+ }
+
+ if ($text !== null) {
+ $error = $text;
+ }
+
+ if ($options['escape']) {
+ $error = h($error);
+ unset($options['escape']);
+ }
+
+ if (is_array($error)) {
+ if (count($error) > 1) {
+ $errorText = [];
+ foreach ($error as $err) {
+ $errorText[] = $this->formatTemplate('errorItem', ['text' => $err]);
+ }
+ $error = $this->formatTemplate('errorList', [
+ 'content' => implode('', $errorText),
+ ]);
+ } else {
+ $error = array_pop($error);
+ }
+ }
+
+ return $this->formatTemplate('error', ['content' => $error]);
+ }
+
+ /**
+ * Returns a formatted LABEL element for HTML forms.
+ *
+ * Will automatically generate a `for` attribute if one is not provided.
+ *
+ * ### Options
+ *
+ * - `for` - Set the for attribute, if its not defined the for attribute
+ * will be generated from the $fieldName parameter using
+ * FormHelper::_domId().
+ * - `escape` - Set to `false` to turn off escaping of label text.
+ * Defaults to `true`.
+ *
+ * Examples:
+ *
+ * The text and for attribute are generated off of the fieldname
+ *
+ * ```
+ * echo $this->Form->label('published');
+ *
+ * ```
+ *
+ * Custom text:
+ *
+ * ```
+ * echo $this->Form->label('published', 'Publish');
+ *
+ * ```
+ *
+ * Custom attributes:
+ *
+ * ```
+ * echo $this->Form->label('published', 'Publish', [
+ * 'for' => 'post-publish'
+ * ]);
+ *
+ * ```
+ *
+ * Nesting an input tag:
+ *
+ * ```
+ * echo $this->Form->label('published', 'Publish', [
+ * 'for' => 'published',
+ * 'input' => $this->text('published'),
+ * ]);
+ *
+ * ```
+ *
+ * If you want to nest inputs in the labels, you will need to modify the default templates.
+ *
+ * @param string $fieldName This should be "modelname.fieldname"
+ * @param string|null $text Text that will appear in the label field. If
+ * $text is left undefined the text will be inflected from the
+ * fieldName.
+ * @param array $options An array of HTML attributes.
+ * @return string The formatted LABEL element
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-labels
+ */
+ public function label(string $fieldName, ?string $text = null, array $options = []): string
+ {
+ if ($text === null) {
+ $text = $fieldName;
+ if (substr($text, -5) === '._ids') {
+ $text = substr($text, 0, -5);
+ }
+ if (strpos($text, '.') !== false) {
+ $fieldElements = explode('.', $text);
+ $text = array_pop($fieldElements);
+ }
+ if (substr($text, -3) === '_id') {
+ $text = substr($text, 0, -3);
+ }
+ $text = __(Inflector::humanize(Inflector::underscore($text)));
+ }
+
+ if (isset($options['for'])) {
+ $labelFor = $options['for'];
+ unset($options['for']);
+ } else {
+ $labelFor = $this->_domId($fieldName);
+ }
+ $attrs = $options + [
+ 'for' => $labelFor,
+ 'text' => $text,
+ ];
+ if (isset($options['input'])) {
+ if (is_array($options['input'])) {
+ $attrs = $options['input'] + $attrs;
+ }
+
+ return $this->widget('nestingLabel', $attrs);
+ }
+
+ return $this->widget('label', $attrs);
+ }
+
+ /**
+ * Generate a set of controls for `$fields`. If $fields is empty the fields
+ * of current model will be used.
+ *
+ * You can customize individual controls through `$fields`.
+ * ```
+ * $this->Form->allControls([
+ * 'name' => ['label' => 'custom label']
+ * ]);
+ * ```
+ *
+ * You can exclude fields by specifying them as `false`:
+ *
+ * ```
+ * $this->Form->allControls(['title' => false]);
+ * ```
+ *
+ * In the above example, no field would be generated for the title field.
+ *
+ * @param array $fields An array of customizations for the fields that will be
+ * generated. This array allows you to set custom types, labels, or other options.
+ * @param array $options Options array. Valid keys are:
+ *
+ * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
+ * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
+ * be enabled
+ * - `legend` Set to false to disable the legend for the generated control set. Or supply a string
+ * to customize the legend text.
+ * @return string Completed form controls.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#generating-entire-forms
+ */
+ public function allControls(array $fields = [], array $options = []): string
+ {
+ $context = $this->_getContext();
+
+ $modelFields = $context->fieldNames();
+
+ $fields = array_merge(
+ Hash::normalize($modelFields),
+ Hash::normalize($fields)
+ );
+
+ return $this->controls($fields, $options);
+ }
+
+ /**
+ * Generate a set of controls for `$fields` wrapped in a fieldset element.
+ *
+ * You can customize individual controls through `$fields`.
+ * ```
+ * $this->Form->controls([
+ * 'name' => ['label' => 'custom label'],
+ * 'email'
+ * ]);
+ * ```
+ *
+ * @param array $fields An array of the fields to generate. This array allows
+ * you to set custom types, labels, or other options.
+ * @param array $options Options array. Valid keys are:
+ *
+ * - `fieldset` Set to false to disable the fieldset. You can also pass an
+ * array of params to be applied as HTML attributes to the fieldset tag.
+ * If you pass an empty array, the fieldset will be enabled.
+ * - `legend` Set to false to disable the legend for the generated input set.
+ * Or supply a string to customize the legend text.
+ * @return string Completed form inputs.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#generating-entire-forms
+ */
+ public function controls(array $fields, array $options = []): string
+ {
+ $fields = Hash::normalize($fields);
+
+ $out = '';
+ foreach ($fields as $name => $opts) {
+ if ($opts === false) {
+ continue;
+ }
+
+ $out .= $this->control($name, (array)$opts);
+ }
+
+ return $this->fieldset($out, $options);
+ }
+
+ /**
+ * Wrap a set of inputs in a fieldset
+ *
+ * @param string $fields the form inputs to wrap in a fieldset
+ * @param array $options Options array. Valid keys are:
+ *
+ * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
+ * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
+ * be enabled
+ * - `legend` Set to false to disable the legend for the generated input set. Or supply a string
+ * to customize the legend text.
+ * @return string Completed form inputs.
+ */
+ public function fieldset(string $fields = '', array $options = []): string
+ {
+ $fieldset = $legend = true;
+ $context = $this->_getContext();
+ $out = $fields;
+
+ if (isset($options['legend'])) {
+ $legend = $options['legend'];
+ }
+ if (isset($options['fieldset'])) {
+ $fieldset = $options['fieldset'];
+ }
+
+ if ($legend === true) {
+ $isCreate = $context->isCreate();
+ $modelName = Inflector::humanize(
+ Inflector::singularize($this->_View->getRequest()->getParam('controller'))
+ );
+ if (!$isCreate) {
+ $legend = __d('cake', 'Edit {0}', $modelName);
+ } else {
+ $legend = __d('cake', 'New {0}', $modelName);
+ }
+ }
+
+ if ($fieldset !== false) {
+ if ($legend) {
+ $out = $this->formatTemplate('legend', ['text' => $legend]) . $out;
+ }
+
+ $fieldsetParams = ['content' => $out, 'attrs' => ''];
+ if (is_array($fieldset) && !empty($fieldset)) {
+ $fieldsetParams['attrs'] = $this->templater()->formatAttributes($fieldset);
+ }
+ $out = $this->formatTemplate('fieldset', $fieldsetParams);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Generates a form control element complete with label and wrapper div.
+ *
+ * ### Options
+ *
+ * See each field type method for more information. Any options that are part of
+ * $attributes or $options for the different **type** methods can be included in `$options` for control().
+ * Additionally, any unknown keys that are not in the list below, or part of the selected type's options
+ * will be treated as a regular HTML attribute for the generated input.
+ *
+ * - `type` - Force the type of widget you want. e.g. `type => 'select'`
+ * - `label` - Either a string label, or an array of options for the label. See FormHelper::label().
+ * - `options` - For widgets that take options e.g. radio, select.
+ * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting
+ * (field error and error messages).
+ * - `empty` - String or boolean to enable empty select box options.
+ * - `nestedInput` - Used with checkbox and radio inputs. Set to false to render inputs outside of label
+ * elements. Can be set to true on any input to force the input inside the label. If you
+ * enable this option for radio buttons you will also need to modify the default `radioWrapper` template.
+ * - `templates` - The templates you want to use for this input. Any templates will be merged on top of
+ * the already loaded templates. This option can either be a filename in /config that contains
+ * the templates you want to load, or an array of templates to use.
+ * - `labelOptions` - Either `false` to disable label around nestedWidgets e.g. radio, multicheckbox or an array
+ * of attributes for the label tag. `selected` will be added to any classes e.g. `class => 'myclass'` where
+ * widget is checked
+ *
+ * @param string $fieldName This should be "modelname.fieldname"
+ * @param array $options Each type of input takes different options.
+ * @return string Completed form widget.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-form-inputs
+ * @psalm-suppress InvalidReturnType
+ * @psalm-suppress InvalidReturnStatement
+ */
+ public function control(string $fieldName, array $options = []): string
+ {
+ $options += [
+ 'type' => null,
+ 'label' => null,
+ 'error' => null,
+ 'required' => null,
+ 'options' => null,
+ 'templates' => [],
+ 'templateVars' => [],
+ 'labelOptions' => true,
+ ];
+ $options = $this->_parseOptions($fieldName, $options);
+ $options += ['id' => $this->_domId($fieldName)];
+
+ $templater = $this->templater();
+ $newTemplates = $options['templates'];
+
+ if ($newTemplates) {
+ $templater->push();
+ $templateMethod = is_string($options['templates']) ? 'load' : 'add';
+ $templater->{$templateMethod}($options['templates']);
+ }
+ unset($options['templates']);
+
+ $error = null;
+ $errorSuffix = '';
+ if ($options['type'] !== 'hidden' && $options['error'] !== false) {
+ if (is_array($options['error'])) {
+ $error = $this->error($fieldName, $options['error'], $options['error']);
+ } else {
+ $error = $this->error($fieldName, $options['error']);
+ }
+ $errorSuffix = empty($error) ? '' : 'Error';
+ unset($options['error']);
+ }
+
+ $label = $options['label'];
+ unset($options['label']);
+
+ $labelOptions = $options['labelOptions'];
+ unset($options['labelOptions']);
+
+ $nestedInput = false;
+ if ($options['type'] === 'checkbox') {
+ $nestedInput = true;
+ }
+ $nestedInput = $options['nestedInput'] ?? $nestedInput;
+ unset($options['nestedInput']);
+
+ if (
+ $nestedInput === true
+ && $options['type'] === 'checkbox'
+ && !array_key_exists('hiddenField', $options)
+ && $label !== false
+ ) {
+ $options['hiddenField'] = '_split';
+ }
+
+ $input = $this->_getInput($fieldName, $options + ['labelOptions' => $labelOptions]);
+ if ($options['type'] === 'hidden' || $options['type'] === 'submit') {
+ if ($newTemplates) {
+ $templater->pop();
+ }
+
+ return $input;
+ }
+
+ $label = $this->_getLabel($fieldName, compact('input', 'label', 'error', 'nestedInput') + $options);
+ if ($nestedInput) {
+ $result = $this->_groupTemplate(compact('label', 'error', 'options'));
+ } else {
+ $result = $this->_groupTemplate(compact('input', 'label', 'error', 'options'));
+ }
+ $result = $this->_inputContainerTemplate([
+ 'content' => $result,
+ 'error' => $error,
+ 'errorSuffix' => $errorSuffix,
+ 'options' => $options,
+ ]);
+
+ if ($newTemplates) {
+ $templater->pop();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Generates an group template element
+ *
+ * @param array $options The options for group template
+ * @return string The generated group template
+ */
+ protected function _groupTemplate(array $options): string
+ {
+ $groupTemplate = $options['options']['type'] . 'FormGroup';
+ if (!$this->templater()->get($groupTemplate)) {
+ $groupTemplate = 'formGroup';
+ }
+
+ return $this->formatTemplate($groupTemplate, [
+ 'input' => $options['input'] ?? [],
+ 'label' => $options['label'],
+ 'error' => $options['error'],
+ 'templateVars' => $options['options']['templateVars'] ?? [],
+ ]);
+ }
+
+ /**
+ * Generates an input container template
+ *
+ * @param array $options The options for input container template
+ * @return string The generated input container template
+ */
+ protected function _inputContainerTemplate(array $options): string
+ {
+ $inputContainerTemplate = $options['options']['type'] . 'Container' . $options['errorSuffix'];
+ if (!$this->templater()->get($inputContainerTemplate)) {
+ $inputContainerTemplate = 'inputContainer' . $options['errorSuffix'];
+ }
+
+ return $this->formatTemplate($inputContainerTemplate, [
+ 'content' => $options['content'],
+ 'error' => $options['error'],
+ 'required' => $options['options']['required'] ? ' required' : '',
+ 'type' => $options['options']['type'],
+ 'templateVars' => $options['options']['templateVars'] ?? [],
+ ]);
+ }
+
+ /**
+ * Generates an input element
+ *
+ * @param string $fieldName the field name
+ * @param array $options The options for the input element
+ * @return string|array The generated input element string
+ * or array if checkbox() is called with option 'hiddenField' set to '_split'.
+ */
+ protected function _getInput(string $fieldName, array $options)
+ {
+ $label = $options['labelOptions'];
+ unset($options['labelOptions']);
+ switch (strtolower($options['type'])) {
+ case 'select':
+ case 'radio':
+ case 'multicheckbox':
+ $opts = $options['options'];
+ if ($opts == null) {
+ $opts = [];
+ }
+ unset($options['options']);
+
+ return $this->{$options['type']}($fieldName, $opts, $options + ['label' => $label]);
+ case 'input':
+ throw new RuntimeException("Invalid type 'input' used for field '$fieldName'");
+
+ default:
+ return $this->{$options['type']}($fieldName, $options);
+ }
+ }
+
+ /**
+ * Generates input options array
+ *
+ * @param string $fieldName The name of the field to parse options for.
+ * @param array $options Options list.
+ * @return array Options
+ */
+ protected function _parseOptions(string $fieldName, array $options): array
+ {
+ $needsMagicType = false;
+ if (empty($options['type'])) {
+ $needsMagicType = true;
+ $options['type'] = $this->_inputType($fieldName, $options);
+ }
+
+ $options = $this->_magicOptions($fieldName, $options, $needsMagicType);
+
+ return $options;
+ }
+
+ /**
+ * Returns the input type that was guessed for the provided fieldName,
+ * based on the internal type it is associated too, its name and the
+ * variables that can be found in the view template
+ *
+ * @param string $fieldName the name of the field to guess a type for
+ * @param array $options the options passed to the input method
+ * @return string
+ */
+ protected function _inputType(string $fieldName, array $options): string
+ {
+ $context = $this->_getContext();
+
+ if ($context->isPrimaryKey($fieldName)) {
+ return 'hidden';
+ }
+
+ if (substr($fieldName, -3) === '_id') {
+ return 'select';
+ }
+
+ $type = 'text';
+ $internalType = $context->type($fieldName);
+ $map = $this->_config['typeMap'];
+ if ($internalType !== null && isset($map[$internalType])) {
+ $type = $map[$internalType];
+ }
+ $fieldName = array_slice(explode('.', $fieldName), -1)[0];
+
+ switch (true) {
+ case isset($options['checked']):
+ return 'checkbox';
+ case isset($options['options']):
+ return 'select';
+ case in_array($fieldName, ['passwd', 'password'], true):
+ return 'password';
+ case in_array($fieldName, ['tel', 'telephone', 'phone'], true):
+ return 'tel';
+ case $fieldName === 'email':
+ return 'email';
+ case isset($options['rows']) || isset($options['cols']):
+ return 'textarea';
+ case $fieldName === 'year':
+ return 'year';
+ }
+
+ return $type;
+ }
+
+ /**
+ * Selects the variable containing the options for a select field if present,
+ * and sets the value to the 'options' key in the options array.
+ *
+ * @param string $fieldName The name of the field to find options for.
+ * @param array $options Options list.
+ * @return array
+ */
+ protected function _optionsOptions(string $fieldName, array $options): array
+ {
+ if (isset($options['options'])) {
+ return $options;
+ }
+
+ $pluralize = true;
+ if (substr($fieldName, -5) === '._ids') {
+ $fieldName = substr($fieldName, 0, -5);
+ $pluralize = false;
+ } elseif (substr($fieldName, -3) === '_id') {
+ $fieldName = substr($fieldName, 0, -3);
+ }
+ $fieldName = array_slice(explode('.', $fieldName), -1)[0];
+
+ $varName = Inflector::variable(
+ $pluralize ? Inflector::pluralize($fieldName) : $fieldName
+ );
+ $varOptions = $this->_View->get($varName);
+ if (!is_iterable($varOptions)) {
+ return $options;
+ }
+ if ($options['type'] !== 'radio') {
+ $options['type'] = 'select';
+ }
+ $options['options'] = $varOptions;
+
+ return $options;
+ }
+
+ /**
+ * Magically set option type and corresponding options
+ *
+ * @param string $fieldName The name of the field to generate options for.
+ * @param array $options Options list.
+ * @param bool $allowOverride Whether or not it is allowed for this method to
+ * overwrite the 'type' key in options.
+ * @return array
+ */
+ protected function _magicOptions(string $fieldName, array $options, bool $allowOverride): array
+ {
+ $options += [
+ 'templateVars' => [],
+ ];
+
+ $options = $this->setRequiredAndCustomValidity($fieldName, $options);
+
+ $typesWithOptions = ['text', 'number', 'radio', 'select'];
+ $magicOptions = (in_array($options['type'], ['radio', 'select'], true) || $allowOverride);
+ if ($magicOptions && in_array($options['type'], $typesWithOptions, true)) {
+ $options = $this->_optionsOptions($fieldName, $options);
+ }
+
+ if ($allowOverride && substr($fieldName, -5) === '._ids') {
+ $options['type'] = 'select';
+ if (!isset($options['multiple']) || ($options['multiple'] && $options['multiple'] !== 'checkbox')) {
+ $options['multiple'] = true;
+ }
+ }
+
+ return $options;
+ }
+
+ /**
+ * Set required attribute and custom validity JS.
+ *
+ * @param string $fieldName The name of the field to generate options for.
+ * @param array $options Options list.
+ * @return array Modified options list.
+ */
+ protected function setRequiredAndCustomValidity(string $fieldName, array $options)
+ {
+ $context = $this->_getContext();
+
+ if (!isset($options['required']) && $options['type'] !== 'hidden') {
+ $options['required'] = $context->isRequired($fieldName);
+ }
+
+ $message = $context->getRequiredMessage($fieldName);
+ $message = h($message);
+
+ if ($options['required'] && $message) {
+ $options['templateVars']['customValidityMessage'] = $message;
+
+ if ($this->getConfig('autoSetCustomValidity')) {
+ $options['data-validity-message'] = $message;
+ $options['oninvalid'] = "this.setCustomValidity(''); "
+ . 'if (!this.value) this.setCustomValidity(this.dataset.validityMessage)';
+ $options['oninput'] = "this.setCustomValidity('')";
+ }
+ }
+
+ return $options;
+ }
+
+ /**
+ * Generate label for input
+ *
+ * @param string $fieldName The name of the field to generate label for.
+ * @param array $options Options list.
+ * @return string|false Generated label element or false.
+ */
+ protected function _getLabel(string $fieldName, array $options)
+ {
+ if ($options['type'] === 'hidden') {
+ return false;
+ }
+
+ $label = null;
+ if (isset($options['label'])) {
+ $label = $options['label'];
+ }
+
+ if ($label === false && $options['type'] === 'checkbox') {
+ return $options['input'];
+ }
+ if ($label === false) {
+ return false;
+ }
+
+ return $this->_inputLabel($fieldName, $label, $options);
+ }
+
+ /**
+ * Extracts a single option from an options array.
+ *
+ * @param string $name The name of the option to pull out.
+ * @param array $options The array of options you want to extract.
+ * @param mixed $default The default option value
+ * @return mixed the contents of the option or default
+ */
+ protected function _extractOption(string $name, array $options, $default = null)
+ {
+ if (array_key_exists($name, $options)) {
+ return $options[$name];
+ }
+
+ return $default;
+ }
+
+ /**
+ * Generate a label for an input() call.
+ *
+ * $options can contain a hash of id overrides. These overrides will be
+ * used instead of the generated values if present.
+ *
+ * @param string $fieldName The name of the field to generate label for.
+ * @param string|array|null $label Label text or array with label attributes.
+ * @param array $options Options for the label element.
+ * @return string Generated label element
+ */
+ protected function _inputLabel(string $fieldName, $label = null, array $options = []): string
+ {
+ $options += ['id' => null, 'input' => null, 'nestedInput' => false, 'templateVars' => []];
+ $labelAttributes = ['templateVars' => $options['templateVars']];
+ if (is_array($label)) {
+ $labelText = null;
+ if (isset($label['text'])) {
+ $labelText = $label['text'];
+ unset($label['text']);
+ }
+ $labelAttributes = array_merge($labelAttributes, $label);
+ } else {
+ $labelText = $label;
+ }
+
+ $labelAttributes['for'] = $options['id'];
+ if (in_array($options['type'], $this->_groupedInputTypes, true)) {
+ $labelAttributes['for'] = false;
+ }
+ if ($options['nestedInput']) {
+ $labelAttributes['input'] = $options['input'];
+ }
+ if (isset($options['escape'])) {
+ $labelAttributes['escape'] = $options['escape'];
+ }
+
+ return $this->label($fieldName, $labelText, $labelAttributes);
+ }
+
+ /**
+ * Creates a checkbox input widget.
+ *
+ * ### Options:
+ *
+ * - `value` - the value of the checkbox
+ * - `checked` - boolean indicate that this checkbox is checked.
+ * - `hiddenField` - boolean to indicate if you want the results of checkbox() to include
+ * a hidden input with a value of ''.
+ * - `disabled` - create a disabled input.
+ * - `default` - Set the default value for the checkbox. This allows you to start checkboxes
+ * as checked, without having to check the POST data. A matching POST data value, will overwrite
+ * the default value.
+ *
+ * @param string $fieldName Name of a field, like this "modelname.fieldname"
+ * @param array $options Array of HTML attributes.
+ * @return string[]|string An HTML text input element.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-checkboxes
+ */
+ public function checkbox(string $fieldName, array $options = [])
+ {
+ $options += ['hiddenField' => true, 'value' => 1];
+
+ // Work around value=>val translations.
+ $value = $options['value'];
+ unset($options['value']);
+ $options = $this->_initInputField($fieldName, $options);
+ $options['value'] = $value;
+
+ $output = '';
+ if ($options['hiddenField']) {
+ $hiddenOptions = [
+ 'name' => $options['name'],
+ 'value' => $options['hiddenField'] !== true
+ && $options['hiddenField'] !== '_split'
+ ? $options['hiddenField'] : '0',
+ 'form' => $options['form'] ?? null,
+ 'secure' => false,
+ ];
+ if (isset($options['disabled']) && $options['disabled']) {
+ $hiddenOptions['disabled'] = 'disabled';
+ }
+ $output = $this->hidden($fieldName, $hiddenOptions);
+ }
+
+ if ($options['hiddenField'] === '_split') {
+ unset($options['hiddenField'], $options['type']);
+
+ return ['hidden' => $output, 'input' => $this->widget('checkbox', $options)];
+ }
+ unset($options['hiddenField'], $options['type']);
+
+ return $output . $this->widget('checkbox', $options);
+ }
+
+ /**
+ * Creates a set of radio widgets.
+ *
+ * ### Attributes:
+ *
+ * - `value` - Indicates the value when this radio button is checked.
+ * - `label` - Either `false` to disable label around the widget or an array of attributes for
+ * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where widget
+ * is checked
+ * - `hiddenField` - boolean to indicate if you want the results of radio() to include
+ * a hidden input with a value of ''. This is useful for creating radio sets that are non-continuous.
+ * - `disabled` - Set to `true` or `disabled` to disable all the radio buttons. Use an array of
+ * values to disable specific radio buttons.
+ * - `empty` - Set to `true` to create an input with the value '' as the first option. When `true`
+ * the radio label will be 'empty'. Set this option to a string to control the label value.
+ *
+ * @param string $fieldName Name of a field, like this "modelname.fieldname"
+ * @param iterable $options Radio button options array.
+ * @param array $attributes Array of attributes.
+ * @return string Completed radio widget set.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-radio-buttons
+ */
+ public function radio(string $fieldName, iterable $options = [], array $attributes = []): string
+ {
+ $attributes['options'] = $options;
+ $attributes['idPrefix'] = $this->_idPrefix;
+ $attributes = $this->_initInputField($fieldName, $attributes);
+
+ $hiddenField = $attributes['hiddenField'] ?? true;
+ unset($attributes['hiddenField']);
+
+ $radio = $this->widget('radio', $attributes);
+
+ $hidden = '';
+ if ($hiddenField) {
+ $hidden = $this->hidden($fieldName, [
+ 'value' => $hiddenField === true ? '' : $hiddenField,
+ 'form' => $attributes['form'] ?? null,
+ 'name' => $attributes['name'],
+ ]);
+ }
+
+ return $hidden . $radio;
+ }
+
+ /**
+ * Missing method handler - implements various simple input types. Is used to create inputs
+ * of various types. e.g. `$this->Form->text();` will create `` while
+ * `$this->Form->range();` will create ``
+ *
+ * ### Usage
+ *
+ * ```
+ * $this->Form->search('User.query', ['value' => 'test']);
+ * ```
+ *
+ * Will make an input like:
+ *
+ * ``
+ *
+ * The first argument to an input type should always be the fieldname, in `Model.field` format.
+ * The second argument should always be an array of attributes for the input.
+ *
+ * @param string $method Method name / input type to make.
+ * @param array $params Parameters for the method call
+ * @return string Formatted input method.
+ * @throws \Cake\Core\Exception\CakeException When there are no params for the method call.
+ */
+ public function __call(string $method, array $params)
+ {
+ $options = [];
+ if (empty($params)) {
+ throw new CakeException(sprintf('Missing field name for FormHelper::%s', $method));
+ }
+ if (isset($params[1])) {
+ $options = $params[1];
+ }
+ if (!isset($options['type'])) {
+ $options['type'] = $method;
+ }
+ $options = $this->_initInputField($params[0], $options);
+
+ return $this->widget($options['type'], $options);
+ }
+
+ /**
+ * Creates a textarea widget.
+ *
+ * ### Options:
+ *
+ * - `escape` - Whether or not the contents of the textarea should be escaped. Defaults to true.
+ *
+ * @param string $fieldName Name of a field, in the form "modelname.fieldname"
+ * @param array $options Array of HTML attributes, and special options above.
+ * @return string A generated HTML text input element
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-textareas
+ */
+ public function textarea(string $fieldName, array $options = []): string
+ {
+ $options = $this->_initInputField($fieldName, $options);
+ unset($options['type']);
+
+ return $this->widget('textarea', $options);
+ }
+
+ /**
+ * Creates a hidden input field.
+ *
+ * @param string $fieldName Name of a field, in the form of "modelname.fieldname"
+ * @param array $options Array of HTML attributes.
+ * @return string A generated hidden input
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-hidden-inputs
+ */
+ public function hidden(string $fieldName, array $options = []): string
+ {
+ $options += ['required' => false, 'secure' => true];
+
+ $secure = $options['secure'];
+ unset($options['secure']);
+
+ $options = $this->_initInputField($fieldName, array_merge(
+ $options,
+ ['secure' => static::SECURE_SKIP]
+ ));
+
+ if ($secure === true && $this->formProtector) {
+ $this->formProtector->addField(
+ $options['name'],
+ true,
+ $options['val'] === false ? '0' : (string)$options['val']
+ );
+ }
+
+ $options['type'] = 'hidden';
+
+ return $this->widget('hidden', $options);
+ }
+
+ /**
+ * Creates file input widget.
+ *
+ * @param string $fieldName Name of a field, in the form "modelname.fieldname"
+ * @param array $options Array of HTML attributes.
+ * @return string A generated file input.
+ * @link https://book.cakephp.org/4/en/views/helpers/form.html#creating-file-inputs
+ */
+ public function file(string $fieldName, array $options = []): string
+ {
+ $options += ['secure' => true];
+ $options = $this->_initInputField($fieldName, $options);
+
+ unset($options['type']);
+
+ return $this->widget('file', $options);
+ }
+
+ /**
+ * Creates a `