@@ -89,3 +116,35 @@
'vv_config' => $vv_config,
]) ?>
+
diff --git a/app/plugins/CoreEnroller/templates/element/field.php b/app/plugins/CoreEnroller/templates/element/field.php
index 80a3b8cd4..2cd01cc27 100644
--- a/app/plugins/CoreEnroller/templates/element/field.php
+++ b/app/plugins/CoreEnroller/templates/element/field.php
@@ -40,9 +40,13 @@
// Do we have a default value configured?
// Either a value or an Environmental Variable,
// Each default value is mutually exclusive to the rest. We do not have to worry about a conflict.
+//
+// NOTE: name/address/telephoneNumber are MVEAs and are rendered as grouped sub-fields.
+// Their per-component defaults must be applied at the sub-field level, not here.
$options['default'] = match(true) {
isset($attr->default_value) => $attr->default_value,
isset($attr->default_value_env_name)
+ && $attr->attribute !== 'name'
&& getenv($attr->default_value_env_name) !== false => getenv($attr->default_value_env_name),
isset($attr->default_value_datetime) => $attr->default_value_datetime,
default => ''
@@ -119,5 +123,5 @@
'formArguments' => $formArguments
]),
// Default use case
- default => $this->Field->getElementsForDisabledInput('form/listItem', $formArguments)
+ default => $this->Field->getElementsForDisabledInput('CoreEnroller.listItem', $formArguments)
};
diff --git a/app/plugins/CoreEnroller/templates/element/fieldDiv.php b/app/plugins/CoreEnroller/templates/element/fieldDiv.php
new file mode 100644
index 000000000..0f1c84764
--- /dev/null
+++ b/app/plugins/CoreEnroller/templates/element/fieldDiv.php
@@ -0,0 +1,75 @@
+
+
+
+ element('form/nameDiv');
+
+ // This configuration isn't necessary anymore.
+ if(isset($vv_field_arguments['fieldDescription'])) {
+ unset($vv_field_arguments['fieldDescription']);
+ $this->set('vv_field_arguments', $vv_field_arguments);
+ }
+
+ // Info Div
+ ?>
+
+ element('form/infoDiv/withPrefix');
+ } elseif(isset($vv_field_arguments['autocomplete'])) {
+ print $this->element('form/infoDiv/autocomplete');
+ } elseif(isset($vv_field_arguments['status'])) {
+ print $this->element('form/infoDiv/status');
+ } elseif(isset($vv_field_arguments['groupedControls'])) {
+ print $this->element('form/infoDiv/grouped');
+ } elseif(isset($vv_field_arguments['entity'])) {
+ print $this->element('form/infoDiv/source');
+ } elseif(isset($vv_field_arguments['groupmember'])) {
+ print $this->element('form/infoDiv/groupMember');
+ } else {
+ print $this->element('form/infoDiv/default');
+ }
+
+ // Insert the afterField supplement:
+ if(!empty($vv_after_field)) {
+ print $vv_after_field;
+ }
+ ?>
+
+
\ No newline at end of file
diff --git a/app/plugins/CoreEnroller/templates/element/listItem.php b/app/plugins/CoreEnroller/templates/element/listItem.php
new file mode 100644
index 000000000..7c02fd4f3
--- /dev/null
+++ b/app/plugins/CoreEnroller/templates/element/listItem.php
@@ -0,0 +1,88 @@
+set('fieldName', $arguments['fieldName']);
+ $fieldName = $arguments['fieldName'];
+ $this->set('vv_field_arguments', $arguments);
+
+ // Pass along the field supplements if they are configured.
+ $this->set('vv_before_field', $beforeField ?? '');
+ $this->set('vv_after_field', $afterField ?? '');
+
+ // If an attribute is frozen, inject a special link to unfreeze it, since
+ // the attribute is read-only and the admin can't simply uncheck the setting
+ if($fieldName == 'frozen' && $this->Field->getEntity()->frozen) {
+ $url = [
+ 'label' => __d('operation', 'unfreeze'),
+ 'url' => [
+ 'plugin' => null,
+ 'controller' => \App\Lib\Util\StringUtilities::entityToClassname($this->Field->getEntity()),
+ 'action' => 'unfreeze',
+ $this->Field->getEntity()->id
+ ]
+ ];
+ $arguments = [
+ ...$arguments,
+ 'status' => __d('field', 'frozen'),
+ 'link' => $url,
+ ];
+ $this->set('vv_field_arguments', $arguments);
+ }
+
+ // If an attribute is a plugin, return the link to its configuration
+ if($fieldName == 'plugin' && $vv_action == 'edit') {
+ $url = [
+ 'label' => __d('operation', 'configure.plugin'),
+ 'url' => [
+ 'plugin' => null,
+ 'controller' => \App\Lib\Util\StringUtilities::entityToClassname($this->Field->getEntity()),
+ 'action' => 'configure',
+ $this->Field->getEntity()->id
+ ]
+ ];
+ $arguments = [
+ ...$arguments,
+ 'status' => $this->Field->getEntity()->$fieldName,
+ 'link' => $url,
+ ];
+ $this->set('vv_field_arguments', $arguments);
+ }
+
+?>
+
+
+ = $this->element('CoreEnroller.fieldDiv')?>
+
diff --git a/app/plugins/CoreEnroller/templates/element/mveas/fieldset-field.php b/app/plugins/CoreEnroller/templates/element/mveas/fieldset-field.php
index e8e7e53e6..82869edc1 100644
--- a/app/plugins/CoreEnroller/templates/element/mveas/fieldset-field.php
+++ b/app/plugins/CoreEnroller/templates/element/mveas/fieldset-field.php
@@ -29,6 +29,7 @@
declare(strict_types = 1);
use \Cake\Utility\Inflector;
+use CoreEnroller\Lib\Util\MagicEnvDefaultUtilities;
// $field: string
// $attr: object
@@ -47,15 +48,29 @@
$isRequiredFromValidationRule = !$modelTable->getValidator()->field($field)->isEmptyAllowed();
}
+// Is the field required?
+$options['required'] = $isRequiredFromValidationRule;
+
// Do we have a default value configured?
// Either a value or an Environmental Variable,
// Each default value is mutually exclusive to the rest. We do not have to worry about a conflict.
+//
+// NOTE: For Name, default_value_env_name is treated as a base env var name.
+// We derive per-component env vars (eg BASE_GIVEN). Missing/empty env vars yield no default.
$options['default'] = match(true) {
isset($attr->default_value) => $attr->default_value,
+
// XXX The $attr->default_value_env_name for the name attribute is tricky. Since the name has many values.
// Check the EnvSource plugin
isset($attr->default_value_env_name)
+ && $attr->attribute === 'name'
+ && MagicEnvDefaultUtilities::nameComponentFromEnv((string)$attr->default_value_env_name, (string)$field) !== null
+ => MagicEnvDefaultUtilities::nameComponentFromEnv((string)$attr->default_value_env_name, (string)$field),
+
+ isset($attr->default_value_env_name)
+ && $attr->attribute !== 'name'
&& getenv($attr->default_value_env_name) !== false => getenv($attr->default_value_env_name),
+
isset($attr->default_value_datetime) => $attr->default_value_datetime,
default => ''
};
diff --git a/app/plugins/CoreEnroller/templates/element/unorderedList.php b/app/plugins/CoreEnroller/templates/element/unorderedList.php
new file mode 100644
index 000000000..d0633c6f1
--- /dev/null
+++ b/app/plugins/CoreEnroller/templates/element/unorderedList.php
@@ -0,0 +1,68 @@
+
+
+
+ $v) {
+ print $this->Form->hidden($attr, ['value' => $v]);
+ }
+ }
\ No newline at end of file
diff --git a/app/plugins/CoreJob/config/plugin.json b/app/plugins/CoreJob/config/plugin.json
new file mode 100644
index 000000000..8cba2a0dc
--- /dev/null
+++ b/app/plugins/CoreJob/config/plugin.json
@@ -0,0 +1,31 @@
+{
+ "types": {
+ "job": [
+ "AdopterJob",
+ "AssignerJob",
+ "DeletionJob",
+ "NesterJob",
+ "ProvisionerJob",
+ "SyncJob"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "sync_job_last_runs": {
+ "comment": "This is a meta-table for SyncJob, and does not have a corresponding MVC",
+ "columns": {
+ "id": {},
+ "external_identity_source_id": {},
+ "job_id": { "type": "integer", "foreignkey": { "table": "jobs", "column": "id" } },
+ "start_time": { "type": "datetime" }
+ },
+ "indexes": {
+ "sync_job_last_runs_i1": { "columns": [ "external_identity_source_id" ] },
+ "sync_job_last_runs_i2": { "needed": false, "columns": [ "job_id" ] }
+ },
+ "changelog": false,
+ "timestamps": false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/CoreJob/resources/locales/en_US/core_job.po b/app/plugins/CoreJob/resources/locales/en_US/core_job.po
index 0598a407b..fe9a7e096 100644
--- a/app/plugins/CoreJob/resources/locales/en_US/core_job.po
+++ b/app/plugins/CoreJob/resources/locales/en_US/core_job.po
@@ -34,9 +34,24 @@ msgstr "Source Keys to process, comma separated (requires external_identity_sour
msgid "opt.assigner.context"
msgstr "Identifier Assignment context"
+msgid "opt.deletion.target_id"
+msgstr "Record ID of Model to delete"
+
+msgid "opt.deletion.target_model"
+msgstr "Model to delete"
+
msgid "opt.entities"
msgstr "Comma separated list of entity IDs to process"
+msgid "opt.nester.group_id"
+msgstr "Nested Group ID"
+
+msgid "opt.nester.target_group_id"
+msgstr "Target Group ID"
+
+msgid "opt.nester.negate"
+msgstr "If true, Negate Nesting"
+
msgid "opt.provisioner.model"
msgstr "Model to provision"
@@ -52,6 +67,9 @@ msgstr "External Identity Source ID (to process a single source)"
msgid "opt.sync.force"
msgstr "If true, force records to process even if no changes have been detected"
+msgid "opt.sync.reference_id"
+msgstr "Reference ID to attach to External Identity (requires exactly one value for source_keys)"
+
msgid "opt.sync.source_keys"
msgstr "Source Keys to process, comma separated (requires external_identity_source_id)"
@@ -91,6 +109,33 @@ msgstr "Assigned {0}: {1}"
msgid "Assigner.start_summary"
msgstr "Assigning all Identifiers in context {0} for CO {1} ({2} entities)"
+msgid "Deletion.error.co"
+msgstr "DeletionJob can only be scheduled from the COmanage CO"
+
+msgid "Deletion.register_summary"
+msgstr "Requested hard delete of {0} {1}"
+
+msgid "Deletion.start_summary"
+msgstr "Beginning hard delete of {0} {1}"
+
+msgid "Deletion.finish_summary"
+msgstr "Deletion completed successfully"
+
+msgid "Nester.recheck.current"
+msgstr "Rechecking existing nested memberships of {0}"
+
+msgid "Nester.recheck.eligible"
+msgstr "Rechecking nesting eligibility of {0} memberships"
+
+msgid "Nester.register_summary"
+msgstr "Requested nesting of Group {0} into target Group {1}"
+
+msgid "Nester.start_summary"
+msgstr "Nesting Group {0} into target Group {1}"
+
+msgid "Nester.finish_summary"
+msgstr "Group Nesting complete"
+
msgid "Provisioner.cancel_summary"
msgstr "Job canceled after provisioning {0} entities ({1} errors)"
@@ -115,6 +160,9 @@ msgstr "Post-Run Tasks failed: {0}"
msgid "Sync.error.pre_run_checks"
msgstr "Pre-Run Checks failed: {0}"
+msgid "Sync.error.reference_id"
+msgstr "reference_id provided but more than one source key was specified"
+
msgid "Sync.finish_summary"
msgstr "Sync Finished"
diff --git a/app/plugins/CoreJob/src/Lib/Jobs/DeletionJob.php b/app/plugins/CoreJob/src/Lib/Jobs/DeletionJob.php
new file mode 100644
index 000000000..aac8c216c
--- /dev/null
+++ b/app/plugins/CoreJob/src/Lib/Jobs/DeletionJob.php
@@ -0,0 +1,103 @@
+ [
+ 'help' => __d('core_job', 'opt.deletion.target_model'),
+ 'type' => 'select',
+ 'choices' => ['Cos', 'GroupNestings'],
+ 'required' => true
+ ],
+ 'target_id' => [
+ 'help' => __d('core_job', 'opt.deletion.target_id'),
+ 'type' => 'integer',
+ 'required' => true
+ ]
+ ];
+ }
+
+ /**
+ * Run the requested Job.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param JobsTable $JobsTable Jobs table, for updating the Job status
+ * @param JobHistoryRecordsTable $JobHistoryRecordsTable Job History Records table, for recording additional history
+ * @param Job $job Job entity
+ * @param array $parameters Parameters for this Job
+ * @throws InvalidArgumentException
+ */
+
+ public function run(
+ \App\Model\Table\JobsTable $JobsTable,
+ \App\Model\Table\JobHistoryRecordsTable $JobHistoryRecordsTable,
+ \App\Model\Entity\Job $job,
+ array $parameters
+ ) {
+ if($parameters['target_model'] == 'Cos') {
+ // Check that the requesting CO is the COmanage CO.
+
+ $requestingCO = $JobsTable->Cos->get($job->co_id);
+
+ if(!$requestingCO->isCOmanageCO()) {
+ throw new \InvalidArgumentException(__d('core_job', 'Deletion.error.co'));
+ }
+ }
+
+ // Find the table to call delete on, and then pull the entity so we can delete it
+ $TargetTable = TableRegistry::getTableLocator()->get($parameters['target_model']);
+
+ // get() will throw an exception on an invalid CO ID
+ $entity = $TargetTable->get($parameters['target_id']);
+
+ $JobsTable->start(job: $job, summary: __d('core_job', 'Deletion.start_summary', [$parameters['target_model'], $parameters['target_id']]));
+
+ // CosTable implements a custom deleteOrFail() that will perform a hard delete,
+ // any other model will revert to the default Cake call which ChangelogBehavior will intercept.
+ $TargetTable->deleteOrFail($entity, ['jobId' => $job->id]);
+
+ $JobsTable->finish(job: $job, summary: __d('core_job', 'Deletion.finish_summary'));
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/CoreJob/src/Lib/Jobs/NesterJob.php b/app/plugins/CoreJob/src/Lib/Jobs/NesterJob.php
new file mode 100644
index 000000000..522fdac75
--- /dev/null
+++ b/app/plugins/CoreJob/src/Lib/Jobs/NesterJob.php
@@ -0,0 +1,100 @@
+ [
+ 'help' => __d('core_job', 'opt.nester.group_id'),
+ 'type' => 'integer',
+ 'required' => true
+ ],
+ 'target_group_id' => [
+ 'help' => __d('core_job', 'opt.nester.target_group_id'),
+ 'type' => 'integer',
+ 'required' => true
+ ],
+ 'negate' => [
+ 'help' => __d('core_job', 'opt.nester.negate'),
+ 'type' => 'bool',
+ 'required' => false
+ ]
+ ];
+ }
+
+ /**
+ * Run the requested Job.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param JobsTable $JobsTable Jobs table, for updating the Job status
+ * @param JobHistoryRecordsTable $JobHistoryRecordsTable Job History Records table, for recording additional history
+ * @param Job $job Job entity
+ * @param array $parameters Parameters for this Job
+ * @throws InvalidArgumentException
+ */
+
+ public function run(
+ \App\Model\Table\JobsTable $JobsTable,
+ \App\Model\Table\JobHistoryRecordsTable $JobHistoryRecordsTable,
+ \App\Model\Entity\Job $job,
+ array $parameters
+ ) {
+ // GMR-2 will prevent nesting Groups across COs, all we need to do is create a
+ // Group Nesting and save it.
+
+ $GroupNestings = TableRegistry::getTableLocator()->get('GroupNestings');
+
+ $nesting = $GroupNestings->newEntity([
+ 'group_id' => $parameters['group_id'],
+ 'target_group_id' => $parameters['target_group_id'],
+ 'negate' => isset($parameters['negate']) && $parameters['negate']
+ ]);
+
+ $JobsTable->start(job: $job, summary: __d('core_job', 'Nester.start_summary', [$parameters['group_id'], $parameters['target_group_id']]));
+
+ $GroupNestings->save($nesting, ['job' => $job]);
+
+ $JobsTable->finish(job: $job, summary: __d('core_job', 'Nester.finish_summary'));
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/CoreJob/src/Lib/Jobs/ProvisionerJob.php b/app/plugins/CoreJob/src/Lib/Jobs/ProvisionerJob.php
index d7ff2356d..a9a6393a0 100644
--- a/app/plugins/CoreJob/src/Lib/Jobs/ProvisionerJob.php
+++ b/app/plugins/CoreJob/src/Lib/Jobs/ProvisionerJob.php
@@ -33,6 +33,7 @@
use Cake\ORM\TableRegistry;
use \App\Lib\Enum\JobStatusEnum;
use \App\Lib\Enum\ProvisionerModeEnum;
+use \App\Lib\Enum\ProvisioningContextEnum;
class ProvisionerJob {
/**
@@ -97,7 +98,12 @@ protected function processEntity(
int &$lastPct
): bool {
try {
- $EntityTable->requestProvisioning($entityId, $model, $target->id);
+ $EntityTable->requestProvisioning(
+ id: $entityId,
+ context: ProvisioningContextEnum::Queue,
+ provisioningTargetId: $target->id,
+ job: $job
+ );
$JobHistoryRecordsTable->record(
jobId: $job->id,
@@ -186,9 +192,17 @@ public function run(
}
if(!empty($parameters['entities'])) {
- // We have one or more explicitly specified entities to process
+ // We have one or more explicitly specified entities to process.
+ // Entities might be an int (single request) or comma separated string
+ // (because PHP).
+
+ $ids = [];
- $ids = explode(',', $parameters['entities']);
+ if(is_int($parameters['entities'])) {
+ $ids[] = $parameters['entities'];
+ } else {
+ $ids = explode(',', $parameters['entities']);
+ }
$count = count($ids);
diff --git a/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php b/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php
index c762cdcb0..64a5b8179 100644
--- a/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php
+++ b/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php
@@ -61,7 +61,11 @@ public function parameterFormat(): array {
'type' => 'bool',
'required' => false
],
-// XXX addd reference_id
+ 'reference_id' => [
+ 'help' => __d('core_job', 'opt.sync.reference_id'),
+ 'type' => 'string',
+ 'required' => false
+ ],
'source_keys' => [
'help' => __d('core_job', 'opt.sync.source_keys'),
'type' => 'string',
@@ -280,7 +284,7 @@ protected function getRunContext(
$this->runContext->eis = $this->runContext->EISTable->get(
$eisId,
- ['contain' => 'SqlSources']
+ contain: $this->runContext->EISTable->getPluginRelations()
);
}
@@ -455,6 +459,16 @@ public function run(
$keys = explode(',', $parameters['source_keys']);
+ $referenceId = null;
+
+ if(!empty($parameters['reference_id'])) {
+ if(count($keys) == 1) {
+ $referenceId = $parameters['reference_id'];
+ } else {
+ throw new \InvalidArgumentException('core_job', 'Sync.error.reference_id');
+ }
+ }
+
$this->runContext->count = count($keys);
$JobsTable->start(
@@ -463,7 +477,7 @@ public function run(
);
foreach($keys as $key) {
- if(!$this->syncRecord($key)) {
+ if(!$this->syncRecord($key, $referenceId)) {
break;
}
}
@@ -501,20 +515,25 @@ public function run(
* Sync a single record.
*
* @since COmanage Registry v5.0.0
- * @param string $key Source Key to process
+ * @param string $key Source Key to process
+ * @param string $referenceId Reference ID to link to record, if known
* @return bool True if processing should continue, false otherwise
*/
- protected function syncRecord(string $key): bool {
+ protected function syncRecord(string $key, string $referenceId=null): bool {
// comment and status for HistoryRecords
$c = "unknown";
$s = JobStatusEnum::Failed;
+ // Default result
+ $result = 'error';
+
try {
$result = $this->runContext->EISTable->sync(
id: (int)$this->runContext->parameters['external_identity_source_id'],
sourceKey: $key,
- force: $this->runContext->force
+ force: $this->runContext->force,
+ referenceId: $referenceId
);
switch($result) {
diff --git a/app/plugins/CoreJob/src/config/plugin.json b/app/plugins/CoreJob/src/config/plugin.json
deleted file mode 100644
index 307e6a147..000000000
--- a/app/plugins/CoreJob/src/config/plugin.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "types": {
- "job": [
- "AdopterJob",
- "AssignerJob",
- "ProvisionerJob",
- "SyncJob"
- ]
- },
- "schema": {
- "tables": {
- "sync_job_last_runs": {
- "comment": "This is a meta-table for SyncJob, and does not have a corresponding MVC",
- "columns": {
- "id": {},
- "external_identity_source_id": {},
- "job_id": { "type": "integer", "foreignkey": { "table": "jobs", "column": "id" } },
- "start_time": { "type": "datetime" }
- },
- "indexes": {
- "sync_job_last_runs_i1": { "columns": [ "external_identity_source_id" ] },
- "sync_job_last_runs_i2": { "needed": false, "columns": [ "job_id" ] }
- },
- "changelog": false,
- "timestamps": false
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/plugins/CoreServer/config/plugin.json b/app/plugins/CoreServer/config/plugin.json
new file mode 100644
index 000000000..9c5fce7c7
--- /dev/null
+++ b/app/plugins/CoreServer/config/plugin.json
@@ -0,0 +1,107 @@
+{
+ "types": {
+ "server": [
+ "HttpServers",
+ "MatchServers",
+ "Oauth2Servers",
+ "SmtpServers",
+ "SqlServers"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "http_servers": {
+ "columns": {
+ "id": {},
+ "server_id": {},
+ "url": { "type": "url" },
+ "username": {},
+ "password": {},
+ "auth_type": { "type": "string", "size": 2 },
+ "skip_ssl_verification": { "type": "boolean" }
+ },
+ "indexes": {
+ "http_servers_i1": { "columns": [ "server_id" ]}
+ }
+ },
+ "match_servers": {
+ "columns": {
+ "id": {},
+ "server_id": {},
+ "url": { "type": "url" },
+ "username": {},
+ "password": {},
+ "auth_type": { "type": "string", "size": 2 },
+ "skip_ssl_verification": { "type": "boolean" }
+ },
+ "indexes": {
+ "match_servers_i1": { "columns": [ "server_id" ] }
+ }
+ },
+ "match_server_attributes": {
+ "columns": {
+ "id": {},
+ "match_server_id": { "type": "integer", "foreignkey": { "table": "match_servers", "column": "id" }, "notnull": true },
+ "attribute": { "type": "string", "size": 80 },
+ "type_id": { "notnull": false },
+ "required": {}
+ },
+ "indexes": {
+ "match_server_attributes_i1": { "columns": [ "match_server_id" ]},
+ "match_server_attributes_i2": { "needed": false, "columns": [ "type_id" ]}
+ },
+ "clone_relation": true
+ },
+ "oauth2_servers": {
+ "columns": {
+ "id": {},
+ "server_id": { "type": "integer" },
+ "url": { "type": "url" },
+ "clientid": { "type": "string", "size": 120 },
+ "client_secret": { "type": "string", "size": 80 },
+ "access_grant_type": { "type": "string", "size": 2 },
+ "scope": { "type": "string", "size": 256 },
+ "refresh_token": { "type": "text" },
+ "access_token": { "type": "text" },
+ "token_response": { "type": "text" },
+ "access_token_exp": { "type": "bigint"}
+ },
+ "indexes": {
+ "oauth2_servers_i1": { "columns": [ "server_id" ] }
+ }
+ },
+ "smtp_servers": {
+ "columns": {
+ "id": {},
+ "server_id": {},
+ "hostname": { "type": "string", "size": 128 },
+ "port": { "type": "integer" },
+ "username": {},
+ "password": {},
+ "use_tls": { "type": "boolean" },
+ "default_from": { "type": "string", "size": 256 },
+ "default_reply_to": { "type": "string", "size": 256 },
+ "override_to": { "type": "string", "size": 256 }
+ },
+ "indexes": {
+ "smtp_servers_i1": { "columns": [ "server_id" ] }
+ }
+ },
+ "sql_servers": {
+ "columns": {
+ "id": {},
+ "server_id": {},
+ "type": { "type": "string", "size": 2 },
+ "hostname": { "type": "string", "size": 128 },
+ "port": { "type": "integer" },
+ "databas": { "type": "string", "size": 128 },
+ "username": {},
+ "password": {}
+ },
+ "indexes": {
+ "sql_servers_i1": { "columns": [ "server_id" ]}
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/CoreServer/resources/locales/en_US/core_server.po b/app/plugins/CoreServer/resources/locales/en_US/core_server.po
index f16da9a10..c6d7ebad1 100644
--- a/app/plugins/CoreServer/resources/locales/en_US/core_server.po
+++ b/app/plugins/CoreServer/resources/locales/en_US/core_server.po
@@ -25,6 +25,9 @@
msgid "controller.HttpServers"
msgstr "{0,plural,=1{HTTP Server} other{HTTP Servers}}"
+msgid "controller.MatchServerAttributes"
+msgstr "{0,plural,=1{Match Server Attribute} other{Match Server Attributes}}"
+
msgid "controller.MatchServers"
msgstr "{0,plural,=1{Match Server} other{Match Servers}}"
@@ -34,6 +37,9 @@ msgstr "{0,plural,=1{SMTP Server} other{SMTP Servers}}"
msgid "controller.SqlServers"
msgstr "{0,plural,=1{SQL Server} other{SQL Servers}}"
+msgid "controller.Oauth2Servers"
+msgstr "{0,plural,=1{Oauth2 Server} other{Oauth2 Servers}}"
+
msgid "enumeration.HttpAuthTypeEnum.BA"
msgstr "Basic"
@@ -43,6 +49,12 @@ msgstr "Bearer"
msgid "enumeration.HttpAuthTypeEnum.X"
msgstr "None"
+msgid "enumeration.GrantTypesEnum.AC"
+msgstr "Authorization Code"
+
+msgid "enumeration.GrantTypesEnum.CC"
+msgstr "Client Credentials"
+
msgid "enumeration.RdbmsTypeEnum.LT"
msgstr "SQLite"
@@ -61,14 +73,68 @@ msgstr "Oracle"
msgid "enumeration.RdbmsTypeEnum.PG"
msgstr "Postgres"
+msgid "error.MatchServers.attr.none"
+msgstr "No valid attributes assembled for Match request"
+
+msgid "error.MatchServers.attr.req"
+msgstr "Required attribute {0} ({1}) not found in person record"
+
+msgid "error.MatchServers.response"
+msgstr "Match Server responded: {0}"
+
+msgid "error.Oauth2Servers.callback"
+msgstr "Incorrect parameters in callback"
+
+msgid "error.Oauth2Servers.state"
+msgstr "Invalid state received in callback"
+
+msgid "error.Oauth2Servers.token"
+msgstr "Error obtaining access token: {0}"
+
+msgid "error.SqlServers.oracle.enabled"
+msgstr "Oracle support is not enabled"
+
+msgid "error.SqlServers.oracle.plugin"
+msgstr "OracleClient plugin is not loaded"
+
+msgid "info.Oauth2Servers.token.ok"
+msgstr "Access Token Obtained"
+
+msgid "info.Oauth2Servers.token.obtain"
+msgstr "Obtain New Token"
+
msgid "field.auth_type"
msgstr "Authentication Type"
msgid "field.skip_ssl_verification"
msgstr "Skip SSL Verification"
-msgid "field.MatchServers.api_endpoint"
-msgstr "API Endpoint"
+msgid "field.Oauth2Servers.access_token"
+msgstr "Access Token"
+
+msgid "field.Oauth2Servers.access_token.desc"
+msgstr "Save any changes to the configuration before obtaining a new token"
+
+msgid "field.Oauth2Servers.clientid"
+msgstr "Client ID"
+
+msgid "field.Oauth2Servers.client_secret"
+msgstr "Client Secret"
+
+msgid "field.Oauth2Servers.access_grant_type"
+msgstr "Access Token Grant Type"
+
+msgid "field.Oauth2Servers.url"
+msgstr "Server URL"
+
+msgid "field.Oauth2Servers.redirect_uri"
+msgstr "Redirect URI"
+
+msgid "field.Oauth2Servers.scope"
+msgstr "Scopes"
+
+msgid "info.Oauth2Servers.access_token.ok"
+msgstr "Access Token Obtained"
msgid "field.SmtpServers.default_from"
msgstr "Default From Address"
@@ -85,12 +151,6 @@ msgstr "If set, all outgoing email will only be sent to this address"
msgid "field.SmtpServers.use_tls"
msgstr "Use TLS"
-msgid "error.SqlServers.oracle.enabled"
-msgstr "Oracle support is not enabled"
-
-msgid "error.SqlServers.oracle.plugin"
-msgstr "OracleClient plugin is not loaded"
-
msgid "field.SqlServers.databas"
msgstr "Database Name"
@@ -99,3 +159,6 @@ msgstr "Specify the port only if a non-standard port number is in use"
msgid "field.SqlServers.type"
msgstr "RDBMS Type"
+
+msgid "result.MatchServers.match.accepted"
+msgstr "Match request requires administrator intervention, Match Request ID: {0}"
\ No newline at end of file
diff --git a/app/plugins/CoreServer/src/Controller/HttpServersController.php b/app/plugins/CoreServer/src/Controller/HttpServersController.php
index 06bbb5fd5..be6405551 100644
--- a/app/plugins/CoreServer/src/Controller/HttpServersController.php
+++ b/app/plugins/CoreServer/src/Controller/HttpServersController.php
@@ -30,11 +30,33 @@
namespace CoreServer\Controller;
use App\Controller\StandardPluginController;
+use Cake\Event\EventInterface;
class HttpServersController extends StandardPluginController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'HttpServers.url' => 'asc'
]
];
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @return Response|void
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->HttpServers->Servers->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->HttpServers->Servers->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->HttpServers->Servers->getPrimaryKey());
+ }
+
+ return parent::beforeRender($event);
+ }
}
diff --git a/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php b/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php
new file mode 100644
index 000000000..cd770d8db
--- /dev/null
+++ b/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php
@@ -0,0 +1,81 @@
+ [
+ 'MatchServerAttributes.attribute' => 'asc'
+ ]
+ ];
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param EventInterface $event Cake Event
+ * @return \Cake\Http\Response HTTP Response
+ */
+
+ public function beforeRender(\Cake\Event\EventInterface $event)
+ {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->MatchServerAttributes->MatchServers->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->MatchServerAttributes->MatchServers->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->MatchServerAttributes->MatchServers->getPrimaryKey());
+ }
+
+ // Build standard server breadcrumbs from *_server_id
+ $customParents = $this->buildServerParamBreadcrumbs();
+
+ if (!empty($customParents)) {
+ $vv_bc_parents = (array)$this->viewBuilder()->getVar('vv_bc_parents');
+ $vv_bc_parents = [...$customParents, ...$vv_bc_parents];
+ $this->set('vv_bc_parents', $vv_bc_parents);
+ }
+
+ $title = __d('core_server', 'controller.MatchServerAttributes', [99]);
+ if(in_array($this->request->getParam('action'), ['add', 'edit'])) {
+ $title = __d('operation', strtolower($this->request->getParam('action')) . '.a', [$title]);
+ }
+
+ $this->set('vv_title', $title);
+
+ return parent::beforeRender($event);
+ }
+}
diff --git a/app/plugins/CoreServer/src/Controller/MatchServersController.php b/app/plugins/CoreServer/src/Controller/MatchServersController.php
index 4b7bc526a..be2e29638 100644
--- a/app/plugins/CoreServer/src/Controller/MatchServersController.php
+++ b/app/plugins/CoreServer/src/Controller/MatchServersController.php
@@ -21,7 +21,7 @@
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry-plugins
- * @since COmanage Registry v5.2.0
+ * @since COmanage Registry v5.1.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
@@ -33,7 +33,7 @@
use Cake\Event\EventInterface;
class MatchServersController extends StandardPluginController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'MatchServers.url' => 'asc'
]
@@ -45,14 +45,17 @@ class MatchServersController extends StandardPluginController {
* @param EventInterface $event Cake Event
*
* @return Response|void
- * @since COmanage Registry v5.2.0
+ * @since COmanage Registry v5.0.0
*/
public function beforeRender(EventInterface $event) {
- // Generate the callback URL
+ $link = $this->getPrimaryLink(true);
-// XXX this needs to be updated for whereever the new API lands
- $this->set('vv_api_endpoint', \Cake\Routing\Router::url('/', true) . 'api/co/' . $this->getCOID() . '/core/v1/resolution');
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->MatchServers->Servers->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->MatchServers->Servers->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->MatchServers->Servers->getPrimaryKey());
+ }
return parent::beforeRender($event);
}
diff --git a/app/plugins/CoreServer/src/Controller/Oauth2ServersController.php b/app/plugins/CoreServer/src/Controller/Oauth2ServersController.php
new file mode 100644
index 000000000..40c2cbfcb
--- /dev/null
+++ b/app/plugins/CoreServer/src/Controller/Oauth2ServersController.php
@@ -0,0 +1,190 @@
+ [
+ 'OauthServers.url' => 'asc'
+ ]
+ ];
+
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->Oauth2Servers->Servers->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->Oauth2Servers->Servers->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->Oauth2Servers->Servers->getPrimaryKey());
+ }
+
+ // Generate the Redirect URI
+ if ($this->getRequest()->getParam('action') === 'edit') {
+ $id = $this->getRequest()->getParam('pass')[0] ?? null; // Assuming $id comes from passed arguments
+ $this->set('vv_redirect_uri', $this->Oauth2Servers->redirectUri($id));
+ }
+
+ return parent::beforeRender($event);
+ }
+
+ /**
+ * OAuth callback.
+ *
+ * @param integer $id Oauth2Server ID
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function callback($id): void
+ {
+ $this->autoRender = false;
+ try {
+ $request = $this->getRequest();
+ $code = $request->getQuery('code');
+ $state = $request->getQuery('state');
+
+ if (empty($code) || empty($state)) {
+ throw new \RuntimeException(__d('core_server', 'error.Oauth2Servers.callback'));
+ }
+
+ // Verify that state is our hashed session ID, as per RFC6749 §10.12
+ // recommendations to prevent CSRF.
+ // https://tools.ietf.org/html/rfc6749#section-10.12
+
+ // Access session from the request object
+ $sessionId = $request->getSession()->id();
+
+ if ($state != hash('sha256', $sessionId)) {
+ throw new \RuntimeException(__d('core_server', 'error.Oauth2Servers.state'));
+ }
+
+ $response = $this->Oauth2Servers->exchangeCode(
+ $id,
+ $code,
+ $this->Oauth2Servers->redirectUri((int)$id),
+ );
+
+ $this->Flash->success(__d('core_server', 'info.Oauth2Servers.access_token.ok'));
+ } catch (\Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ $this->performRedirect();
+ }
+
+ /**
+ * Obtain an access token for a Oauth2Server.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param integer $id Oauth2Server ID
+ */
+
+ public function token($id): void
+ {
+ // Pull our configuration, initially to find out what type of grant type we need
+ $osrvr = $this->Oauth2Servers->get($id);
+
+ if(!$osrvr) {
+ $this->Flash->error(__d('error', 'notfound', [__d('core_server', 'controller.Oauth2Servers')]));
+ $this->performRedirect();
+ }
+
+ try {
+ switch($osrvr->access_grant_type) {
+ case GrantTypesEnum::AuthorizationCode:
+ // Issue a redirect to the server
+ $targetUrl = $osrvr->url
+ . '/authorize?response_type=code'
+ . '&client_id=' . $osrvr->clientid
+ . '&redirect_uri=' . urlencode($this->Oauth2Servers->redirectUri($id))
+ . '&state=' . hash('sha256', session_id());
+ // Scope is optional
+ if(!empty($osrvr->scope)) {
+ $targetUrl .= '&scope='. str_replace(' ', '%20', $osrvr->scope);
+ }
+
+ $this->redirect($targetUrl);
+ break;
+ case GrantTypesEnum::ClientCredentials:
+ // Make a direct call to the server
+ $this->Oauth2Servers->obtainToken((int)$id, 'client_credentials');
+ $this->Flash->success(__d('core_server', 'info.Oauth2Servers.access_token.ok'));
+ break;
+ default:
+ // No other flows currently supported
+ throw new \LogicException('Not implemented yet.');
+ }
+ } catch(\Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ $this->performRedirect();
+ }
+
+ /**
+ * Perform a redirect back to the controller's default view.
+ *
+ * @since COmanage Registry v5.2.0
+ */
+
+ function performRedirect(): void
+ {
+ $target = [];
+ $target['plugin'] = null;
+
+ if (!empty($this->getRequest()->getParam('pass')[0])) {
+ $target['plugin'] = 'CoreServer';
+ $target['controller'] = 'Oauth2Servers';
+ $target['action'] = 'edit';
+ $target[] = filter_var($this->getRequest()->getParam('pass')[0], FILTER_SANITIZE_SPECIAL_CHARS);
+ } else {
+ $target['controller'] = 'Servers';
+ $target['action'] = 'index';
+ $target['?'] = [
+ 'co_id' => $this->getCOID()
+ ];
+ }
+
+ $this->redirect($target);
+ }
+}
diff --git a/app/plugins/CoreServer/src/Controller/SmtpServersController.php b/app/plugins/CoreServer/src/Controller/SmtpServersController.php
index c472e18df..bca8b47ab 100644
--- a/app/plugins/CoreServer/src/Controller/SmtpServersController.php
+++ b/app/plugins/CoreServer/src/Controller/SmtpServersController.php
@@ -30,11 +30,33 @@
namespace CoreServer\Controller;
use App\Controller\StandardPluginController;
+use Cake\Event\EventInterface;
class SmtpServersController extends StandardPluginController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'SmtpServers.hostname' => 'asc'
]
];
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @return Response|void
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->SmtpServers->Servers->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->SmtpServers->Servers->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->SmtpServers->Servers->getPrimaryKey());
+ }
+
+ return parent::beforeRender($event);
+ }
}
diff --git a/app/plugins/CoreServer/src/Controller/SqlServersController.php b/app/plugins/CoreServer/src/Controller/SqlServersController.php
index f37690a7b..9e840242f 100644
--- a/app/plugins/CoreServer/src/Controller/SqlServersController.php
+++ b/app/plugins/CoreServer/src/Controller/SqlServersController.php
@@ -30,11 +30,33 @@
namespace CoreServer\Controller;
use App\Controller\StandardPluginController;
+use Cake\Event\EventInterface;
class SqlServersController extends StandardPluginController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'SqlServers.hostname' => 'asc'
]
];
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @return Response|void
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->SqlServers->Servers->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->SqlServers->Servers->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->SqlServers->Servers->getPrimaryKey());
+ }
+
+ return parent::beforeRender($event);
+ }
}
diff --git a/app/plugins/CoreServer/src/Lib/Enum/GrantTypesEnum.php b/app/plugins/CoreServer/src/Lib/Enum/GrantTypesEnum.php
new file mode 100644
index 000000000..219c2264f
--- /dev/null
+++ b/app/plugins/CoreServer/src/Lib/Enum/GrantTypesEnum.php
@@ -0,0 +1,39 @@
+
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/CoreServer/src/Model/Entity/MatchServer.php b/app/plugins/CoreServer/src/Model/Entity/MatchServer.php
index ceab5bf4f..8b01f27fc 100644
--- a/app/plugins/CoreServer/src/Model/Entity/MatchServer.php
+++ b/app/plugins/CoreServer/src/Model/Entity/MatchServer.php
@@ -21,7 +21,7 @@
*
* @link https://www.internet2.edu/comanage COmanage Project
* @package registry-plugins
- * @since COmanage Registry v5.2.0
+ * @since COmanage Registry v5.1.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
@@ -32,6 +32,8 @@
use Cake\ORM\Entity;
class MatchServer extends Entity {
+ use \App\Lib\Traits\EntityMetaTrait;
+
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -41,7 +43,7 @@ class MatchServer extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php b/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php
new file mode 100644
index 000000000..932135764
--- /dev/null
+++ b/app/plugins/CoreServer/src/Model/Entity/MatchServerAttribute.php
@@ -0,0 +1,53 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+
+ public array $_supportedMetadata = ['clonable-related'];
+}
diff --git a/app/plugins/CoreServer/src/Model/Entity/Oauth2Server.php b/app/plugins/CoreServer/src/Model/Entity/Oauth2Server.php
new file mode 100644
index 000000000..b4d0521ec
--- /dev/null
+++ b/app/plugins/CoreServer/src/Model/Entity/Oauth2Server.php
@@ -0,0 +1,51 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/CoreServer/src/Model/Entity/SmtpServer.php b/app/plugins/CoreServer/src/Model/Entity/SmtpServer.php
index f46d72386..ecb779c3b 100644
--- a/app/plugins/CoreServer/src/Model/Entity/SmtpServer.php
+++ b/app/plugins/CoreServer/src/Model/Entity/SmtpServer.php
@@ -32,6 +32,8 @@
use Cake\ORM\Entity;
class SmtpServer extends Entity {
+ use \App\Lib\Traits\EntityMetaTrait;
+
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -41,7 +43,7 @@ class SmtpServer extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/CoreServer/src/Model/Entity/SqlServer.php b/app/plugins/CoreServer/src/Model/Entity/SqlServer.php
index 66ee6b241..8f0c811ca 100644
--- a/app/plugins/CoreServer/src/Model/Entity/SqlServer.php
+++ b/app/plugins/CoreServer/src/Model/Entity/SqlServer.php
@@ -32,6 +32,8 @@
use Cake\ORM\Entity;
class SqlServer extends Entity {
+ use \App\Lib\Traits\EntityMetaTrait;
+
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -41,7 +43,7 @@ class SqlServer extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/CoreServer/src/Model/Table/HttpServersTable.php b/app/plugins/CoreServer/src/Model/Table/HttpServersTable.php
index 06802e7d1..6eeff5cd7 100644
--- a/app/plugins/CoreServer/src/Model/Table/HttpServersTable.php
+++ b/app/plugins/CoreServer/src/Model/Table/HttpServersTable.php
@@ -41,6 +41,7 @@
class HttpServersTable extends Table {
use \App\Lib\Traits\AutoViewVarsTrait;
use \App\Lib\Traits\CoLinkTrait;
+ use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\TableMetaTrait;
@@ -112,17 +113,25 @@ public function createHttpClient(int $id): Client {
$Client = Client::createFromUrl($httpServer->url);
if($httpServer->auth_type == HttpAuthTypeEnum::Basic) {
+ $authConfig = [
+ 'type' => 'Basic'
+ ];
+
if(!empty($httpServer->username)) {
- $Client->setConfig('username', $httpServer->username);
+ $authConfig['username'] = $httpServer->username;
}
if(!empty($httpServer->password)) {
- $Client->setConfig('password', $httpServer->password);
+ $authConfig['password'] = $httpServer->password;
}
+
+ $Client->setConfig('auth', $authConfig);
}
if(!empty($httpServer->skip_ssl_verification) && $httpServer->skip_ssl_verification) {
$Client->setConfig('ssl_verify_peer', false);
+ $Client->setConfig('ssl_verify_peer_name', false);
+ $Client->setConfig('ssl_verify_host', false);
}
return $Client;
diff --git a/app/plugins/CoreServer/src/Model/Table/MatchServerAttributesTable.php b/app/plugins/CoreServer/src/Model/Table/MatchServerAttributesTable.php
new file mode 100644
index 000000000..8909a309c
--- /dev/null
+++ b/app/plugins/CoreServer/src/Model/Table/MatchServerAttributesTable.php
@@ -0,0 +1,239 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ // Timestamp behavior handles created/modified updates
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('CoreServer.MatchServers');
+ $this->belongsTo('Types');
+
+ $this->setDisplayField('attribute');
+
+ $this->setPrimaryLink('CoreServer.match_server_id');
+ $this->setRequiresCO(true);
+ $this->setRedirectGoal('index');
+
+ $this->setAutoViewVars([
+ 'attributes' => [
+ 'type' => 'hash',
+ 'hash' => Hash::combine($this->supportedAttributes(), '{s}.wire', '{s}.label')
+ ],
+ // Because of the way autoviewvars calculates variable names, "requireds" is correct
+ 'requireds' => [
+ 'type' => 'enum',
+ 'class' => 'RequiredEnum'
+ ],
+ 'addressTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'Addresses.type'
+ ],
+ 'emailAddressTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'EmailAddresses.type'
+ ],
+ 'identifierTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'Identifiers.type'
+ ],
+ 'nameTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'Names.type'
+ ],
+ 'telephoneNumberTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'TelephoneNumbers.type'
+ ],
+ 'types' => [
+ 'type' => 'type'
+ ]
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Obtain the set of attributes supported for Match requests.
+ *
+ * @since COmanage Registry v5.2.0
+ * @return array Array of supported attributes
+ */
+
+ public function supportedAttributes(): array {
+ return [
+ // The array key should match the wire name
+ 'addresses' => [
+ 'label' => __d('controller', 'Addresses', [1]),
+ 'model' => 'Addresses',
+ // array values are "registry name" => "attribute dictionary name"
+ 'attributes' => [
+ // We'll use the virtual attribute to get a formatted address.
+ // We could provide individual attributes (similar to Name) but
+ // we don't have any actual use cases for this yet.
+ 'formatted_address' => 'formatted'
+ ],
+ 'type' => 'Addresses.type',
+ 'wire' => 'addresses'
+ ],
+ 'dateOfBirth' => [
+ 'label' => __d('field', 'date_of_birth'),
+ 'model' => 'ExternalIdentities',
+ // Note the use of singular "attribute", vs "attributes" for MVPAs
+ 'attribute' => 'date_of_birth',
+ 'type' => false,
+ 'wire' => 'dateOfBirth'
+ ],
+ 'emailAddresses' => [
+ 'label' => __d('field', 'mail'),
+ 'model' => 'EmailAddresses',
+ 'attributes' => [
+ 'mail' => 'address'
+ ],
+ // type corresponds to Types::$supportedAttributes, and will automatically
+ // be injected into the wire representation (ie: do not list it under
+ // "attributes")
+ 'type' => 'EmailAddresses.type',
+ 'wire' => 'emailAddresses'
+ ],
+ 'identifiers' => [
+ 'label' => __d('field', 'identifier'),
+ 'model' => 'Identifiers',
+ 'attributes' => [
+ 'identifier' => 'identifier'
+ ],
+ 'type' => 'Identifiers.type',
+ 'wire' => 'identifiers'
+ ],
+ 'names' => [
+ 'label' => __d('field', 'name'),
+ 'model' => 'Names',
+ 'attributes' => [
+ 'honorific' => 'prefix',
+ 'given' => 'given',
+ 'middle' => 'middle',
+ 'family' => 'family',
+ 'suffix' => 'suffix'
+ ],
+ 'type' => 'Names.type',
+ 'wire' => 'names'
+ ],
+ 'telephoneNumbers' => [
+ 'label' => __d('field', 'TelephoneNumbers.number'),
+ 'model' => 'TelephoneNumbers',
+ 'attributes' => [
+ // We'll use the virtual attribute to get a formatted number
+ 'formatted_number' => 'number'
+ ],
+ 'type' => 'TelephoneNumbers.type',
+ 'wire' => 'telephoneNumbers'
+ ]
+ ];
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('match_server_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('match_server_id');
+
+ $validator->add('attribute', [
+ 'content' => ['rule' => ['inList', Hash::extract($this->supportedAttributes(), '{s}.wire')]]
+ ]);
+ $validator->notEmptyString('attribute');
+
+ $validator->add('type_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ // Required really depends on attribute
+ $validator->allowEmptyString('type_id');
+
+ $validator->add('required', [
+ 'content' => ['rule' => ['inList', RequiredEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('required');
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/CoreServer/src/Model/Table/MatchServersTable.php b/app/plugins/CoreServer/src/Model/Table/MatchServersTable.php
index 6b095d270..e9fe6ff8b 100644
--- a/app/plugins/CoreServer/src/Model/Table/MatchServersTable.php
+++ b/app/plugins/CoreServer/src/Model/Table/MatchServersTable.php
@@ -34,7 +34,11 @@
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
+use Cake\Utility\Hash;
+use Cake\Utility\Inflector;
use Cake\Validation\Validator;
+use App\Lib\Enum\SuspendableStatusEnum;
+use App\Lib\Enum\RequiredEnum;
class MatchServersTable extends HttpServersTable {
/**
@@ -55,7 +59,11 @@ public function initialize(array $config): void {
$this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
// Define associations
- $this->belongsTo('Servers');
+ // this is defined in HttpServersTable
+ // $this->belongsTo('Servers');
+ $this->hasMany('CoreServer.MatchServerAttributes')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
$this->setDisplayField('hostname');
@@ -73,10 +81,338 @@ public function initialize(array $config): void {
'table' => [
'add' => ['platformAdmin', 'coAdmin'],
'index' => ['platformAdmin', 'coAdmin']
+ ],
+ 'related' => [
+ 'table' => [
+ 'CoreServer.MatchServerAttributes'
+ ]
]
]);
}
+ /**
+ * Assemble attributes from a person record into a format suitable for wire
+ * transfer.
+ *
+ * @since COmanage Registry v4.0.0
+ * @param array $matchAttributes Match Attribute configuration
+ * @param array $queryAttributes Identity attributes for querying
+ * @return array Array of data suitable for conversion to JSON
+ * @throws InvalidArgumentException
+ * @throws RuntimeException
+ */
+
+ protected function assembleRequestAttributes(
+ array $matchAttributes,
+ array $queryAttributes
+ ): array {
+ $matchRequest = [];
+
+ $supportedAttrs = $this->MatchServerAttributes->supportedAttributes();
+
+ foreach($matchAttributes as $mattr) {
+ if($mattr->required == RequiredEnum::NotPermitted) {
+ continue;
+ }
+
+ $found = false;
+
+ // This is the key used by supportedAttributes(), which is also the value
+ // stored in the database for 'attribute' by the form
+ $attrKey = $mattr->attribute;
+
+ // $model = (eg) EmailAddresses
+ $model = $supportedAttrs[$attrKey]['model'];
+ // $tableName = (eg) email_addresses
+ $tableName = Inflector::tableize($model);
+ // $wire = (eg) emailAddresses
+ $wire = $supportedAttrs[$attrKey]['wire'];
+
+ if(isset($supportedAttrs[$attrKey]['attribute'])) {
+ // This is a singleton value on OrgIdentity, eg "date_of_birth"
+
+ // XXX date_of_birth is expected to be YYYY-MM-DD but we don't currently try to reformat it...
+
+ if(!empty($queryAttributes[ $supportedAttrs[$attrKey]['attribute'] ])) {
+ $matchRequest['sorAttributes'][$wire] = $queryAttributes[ $supportedAttrs[$attrKey]['attribute'] ];
+ $found = true;
+ }
+ } elseif(isset($supportedAttrs[$attrKey]['attributes'])) {
+ // This is an MVEA, eg "emailAddress" or "telephoneNumber"
+
+ // When assembling attributes from MVEAs, we include all available attributes.
+ // The Match server can ignore the ones it doesn't care about.
+
+ // We don't try to reformat the attribute (strip spaces, slashes, etc) since
+ // the match engine should be configured to treat the attribute appropriately
+ // (eg: alphanumeric). That is, unless a formatting function is specified
+ // to (eg) assemble a TelephoneNumber from its parts.
+
+ // $type = (eg) official (as configured for this Match Server instance)
+ $type = $mattr->type->value;;
+
+ if(!empty($queryAttributes[$tableName])) {
+ $obj = Hash::extract($queryAttributes[$tableName], '{n}[type='.$type.']');
+
+ if(!empty($obj)) {
+ foreach($obj as $o) {
+ // Assemble the record
+ $attrs = ['type' => $type];
+
+ if(!empty($supportedAttrs[$attrKey]['attributes'])) {
+ foreach($supportedAttrs[$attrKey]['attributes'] as $ra => $ad) {
+ // $ra = Registry Attribute, $ad = Attribute Dictionary attribute
+ // We use isset() rather than !empty() to avoid issues with
+ // "blank" values, including 0
+ if(isset($o[$ra])) {
+ $attrs[$ad] = $o[$ra];
+ }
+ }
+ }
+ /* So far we don't need a function in PE since we use virtual attributes
+ on the entity instead
+ else {
+ // Call the function
+ $fn = $supportedAttrs[$attrKey]['function'];
+
+ $attrs[ $supportedAttrs[$attrKey]['wireField'] ] = $fn($o);
+ } */
+
+ // Make sure we have something other than type to work with
+ if(count(array_keys($attrs)) > 1) {
+ $matchRequest['sorAttributes'][$wire][] = $attrs;
+ $found = true;
+ }
+ }
+ }
+ }
+ } else {
+ throw new \LogicException('NOT IMPLEMENTED: ' . $attrKey);
+ }
+
+ if(!$found && $mattr->required == RequiredEnum::Required) {
+ throw new \InvalidArgumentException(__d('core_server', 'error.MatchServers.attr.req', [$attrKey, $mattr->id]));
+ }
+ }
+
+ if(empty($matchRequest)) {
+ // We didn't find any attributes, so throw an error
+
+ throw new \RuntimeException(__d('core_server', 'error.MatchServers.attr.none'));
+ }
+
+ return $matchRequest;
+ }
+
+ /**
+ * Perform an ID Match Reference Identifier or Update Attributes Request.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param int $serverId Server ID (NOT Match Server ID)
+ * @param string $sorLabel System of Record Label
+ * @param string $sorId System of Record ID (Source Key)
+ * @param array $attributes Available attributes used to perform match request with
+ * @param string $referenceId Reference ID, for forced reconciliation request
+ * @param string $action Requested action: "request" (Reference ID) or "update" (Match Attributes)
+ * @return string|array|bool Reference ID or (on 300 response) array of choices, or true (Update request)
+ * @throws InvalidArgumentException
+ * @throws UnexpectedValueException
+ */
+
+ protected function doRequest(
+ int $serverId,
+ string $sorLabel,
+ string $sorId,
+ array $attributes,
+ ?string $referenceId=null,
+ string $action='request'
+ ): string|array|bool {
+ // Pull the Match Server configuration
+
+ $matchServer = $this->Servers->get(
+ $serverId,
+ contain: ['MatchServers' => ['MatchServerAttributes' => 'Types']],
+ );
+
+ if($matchServer->status != SuspendableStatusEnum::Active) {
+ throw new \InvalidArgumentException(__d('error', 'inactive', [__d('controller', 'Servers', [1]), $serverId]));
+ }
+
+ // Assemble the match request attributes using the provided entity attributes
+ // Let any exceptions bubble up
+ $matchRequest = $this->assembleRequestAttributes(
+ $matchServer->match_server->match_server_attributes,
+ $attributes
+ );
+
+ if($referenceId) {
+ // Insert the requested Reference ID into the message body
+
+ $matchRequest['referenceId'] = $referenceId;
+ }
+
+ $MatchClient = $this->createHttpClient($matchServer->match_server->id);
+
+ $url = "/people/" . urlencode($sorLabel) . "/" . urlencode($sorId);
+ $options = [
+ 'headers' =>['Content-Type' => 'application/json']
+ ];
+
+ if($action == 'request') {
+ // Before we submit the PUT, we do a GET (Request Current Values) to see
+ // if there is already a reference ID available. This is primarily to
+ // handle a potential match (202) situation (ie: we previously tried to
+ // get a reference ID but got a 202 instead). Deployers could enable the
+ // Match Callback API, in which case this wouldn't be necessary.
+
+ // An alternate approach here would be to store the Match Request ID
+ // returned as part of the 202 response, however that ID is optional
+ // (maybe it should be required?) and we would need to track it somewhere
+ // (in the OIS Record?). It would be nice to link to the pending request
+ // though...
+
+ $response = $MatchClient->get($url);
+
+ if($response->getStatusCode() == 200) {
+ $body = $response->getJson();
+
+ if(!empty($body['meta']['referenceId'])) {
+ $this->llog('trace', "Received existing Reference ID " . $body['meta']['referenceId'] . " for $sorLabel / $sorId from Match server " . $matchServer->match_server->id);
+
+ // The pending match has been resolved
+ return $body['meta']['referenceId'];
+ }
+ }
+ }
+
+ $response = $MatchClient->put($url, json_encode($matchRequest), $options);
+
+ $body = $response->getJson();
+
+ $this->llog('trace', "Match server " . $matchServer->match_server->id
+ . " returned " . $response->getStatusCode()
+ . " for $sorLabel / $sorId");
+
+ // If we get anything other than a 200/201 back, throw an error. This includes
+ // 202, which we handle by simply generating a slightly different error.
+
+ if($response->getStatusCode() == 202) {
+ $requestId = "?";
+
+ if(!empty($body['matchRequest'])) {
+ // Match Request is an optional part of the response
+ $requestId = $body['matchRequest'];
+ }
+
+ throw new \UnexpectedValueException(__d('core_server', 'result.MatchServers.match.accepted', $requestId));
+ }
+
+ if($response->getStatusCode() == 300) {
+ $candidates = [];
+
+ // Inject the "new" candidate to make it easier for the calling code
+ $candidates[] = [
+ 'referenceId' => 'new',
+ 'sorRecords' => [
+ [
+ 'meta' => [
+ 'referenceId' => 'new'
+ ],
+ 'sorAttributes' => $matchRequest['sorAttributes']
+ ]
+ ]
+ ];
+
+ $candidates = array_merge($candidates, $body['candidates']);
+ return $candidates;
+ }
+
+ if($response->getStatusCode() != 200 && $response->getStatusCode() != 201) {
+ $error = $response->getReasonPhrase();
+
+ // If an error was provided in the response, use that instead
+ if(!empty($body['error'])) {
+ $error = $body['error'];
+ }
+
+ $this->llog('error', "Match Server " . $matchServer->match_server->id . " returned error $error");
+
+ throw new \RuntimeException(__d('core_server', 'error.MatchServers.response', [$error]));
+ }
+
+ if($action == 'request') {
+ // We expect a reference ID
+ $this->llog('trace', "Received Reference ID " . $body['referenceId'] . " for $sorLabel / $sorId from Match server " . $matchServer->match_server->id);
+
+ return $body['referenceId'];
+ }
+
+ // There is no Reference ID returned for an Update Match Attributes request
+ return true;
+ }
+
+ /**
+ * Perform an ID Match Reference Identifier Request.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param integer $serverId Server ID
+ * @param string $sorLabel System of Record Label
+ * @param string $sorId System of Record ID (Source Key)
+ * @param array $attributes Available attributes used to perform match request with
+ * @param string $referenceId Reference ID, for forced reconciliation request
+ * @return string|array Reference ID or (on 300 response) array of choices
+ * @throws InvalidArgumentException
+ * @throws UnexpectedValueException
+ */
+
+ public function requestReferenceIdentifier(
+ int $serverId,
+ string $sorLabel,
+ string $sorId,
+ array $attributes,
+ ?string $referenceId=null
+ ): string|array {
+ return $this->doRequest(
+ serverId: $serverId,
+ sorLabel: $sorLabel,
+ sorId: $sorId,
+ attributes: $attributes,
+ referenceId: $referenceId,
+ action: 'request'
+ );
+ }
+
+ /**
+ * Perform an ID Match Update Match Attributes Request.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param int $serverId Server ID (NOT Match Server ID)
+ * @param string $sorLabel System of Record Label
+ * @param string $sorId System of Record ID (Source Key)
+ * @param array $attributes Available attributes used to perform match request with
+ * @return boolean true on success
+ * @throws InvalidArgumentException
+ */
+
+ public function updateMatchAttributes(
+ int $serverId,
+ string $sorLabel,
+ string $sorId,
+ array $attributes
+ ): bool {
+ // This is basically the same request as requestReferenceIdentifier().
+ // If we don't throw an exception then we successfully processed the request.
+
+ return $this->doRequest(
+ serverId: $serverId,
+ sorLabel: $sorLabel,
+ sorId: $sorId,
+ attributes: $attributes,
+ action: 'update'
+ );
+ }
+
+
/**
* Set validation rules.
*
diff --git a/app/plugins/CoreServer/src/Model/Table/Oauth2ServersTable.php b/app/plugins/CoreServer/src/Model/Table/Oauth2ServersTable.php
new file mode 100644
index 000000000..f31ce438c
--- /dev/null
+++ b/app/plugins/CoreServer/src/Model/Table/Oauth2ServersTable.php
@@ -0,0 +1,287 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ // Timestamp behavior handles created/modified updates
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ // this is defined in HttpServersTable
+ // $this->belongsTo('Servers');
+
+ $this->setDisplayField('hostname');
+
+ $this->setPrimaryLink('server_id');
+ $this->setAllowLookupPrimaryLink(['token', 'callback']);
+
+ $this->setRequiresCO(true);
+
+ $this->setAutoViewVars([
+ 'types' => [
+ 'type' => 'enum',
+ 'class' => 'CoreServer.GrantTypesEnum'
+ ]
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false, // Delete the pluggable object instead
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin'],
+ 'token' => ['platformAdmin', 'coAdmin'],
+ 'callback' => true,
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin'],
+ ]
+ ]);
+ }
+
+ /**
+ * Exchange an authorization code for an access and refresh token.
+ *
+ * @param int|string $id Oauth2Server ID
+ * @param string $code Access code returned by call to /oauth/authorize
+ * @param string $redirectUri Callback URL used for initial request
+ * @return mixed Object of data as returned by server, including access and refresh token
+ * @throws RuntimeException
+ *@since COmanage Registry v5.2.0
+ */
+
+ public function exchangeCode(int|string $id, string $code, string $redirectUri, $store=true): mixed
+ {
+ return $this->obtainToken((int)$id, 'authorization_code', $code, $redirectUri, $store);
+ }
+
+ /**
+ * Obtain an OAuth token.
+ *
+ * @param Integer $id Oauth2Server ID
+ * @param String $grantType OAuth grant type
+ * @param String|null $code Access code returned by call to /oauth/authorize, for authorization_code grant
+ * @param String|null $redirectUri Callback URL used for initial request, for authorization_code grant
+ * @param Boolean $store If true, store the retrieved tokens in the Oauth2Server configuration
+ * @return mixed Object of data as returned by server, including access and refresh token
+ * @throws RuntimeException
+ *@since COmanage Registry v5.2.0
+ */
+
+ public function obtainToken(int $id, string $grantType, string $code=null, string $redirectUri=null, bool $store=true): mixed
+ {
+ // Pull our configuration
+ $srvr = $this->get($id);
+
+ $httpClient = $this->createHttpClient($id);
+
+ $postData = [
+ 'client_id' => $srvr->clientid,
+ 'client_secret' => $srvr->client_secret,
+ 'grant_type' => $grantType
+ ];
+
+ if($grantType == 'refresh_token') {
+ $postData['refresh_token'] = $srvr->refresh_token;
+ $postData['format'] = 'json';
+ } elseif($grantType == 'authorization_code' && $code) {
+ $postData['code'] = $code;
+ $postData['redirect_uri'] = $redirectUri;
+ } else {
+ $postData['scope'] = str_replace(' ', '%20', $srvr->scope);
+ }
+
+ $postUrl = $srvr->url . "/token";
+
+ $results = $httpClient->post($postUrl, $postData);
+
+ $json = json_decode($results->getStringBody());
+
+ if($results->getStatusCode() != 200) {
+ // There should be an error in the response
+ throw new \RuntimeException(__d('core_server', 'error.Oauth2Servers.token', [$json->error . ": " . $json->error_description]));
+ }
+
+ if($store) {
+ // Save the fields we want to keep
+ $data = [
+ 'id' => $id,
+ 'access_token' => $json->access_token,
+ // Store the raw result in case the server has added some custom attributes
+ 'token_response' => json_encode($json)
+ ];
+
+ // We shouldn't have a new refresh token on a refresh_token grant
+ // (which just gets us a new access token). Additionally, section
+ // 4.4.3 of RFC 6749 explains that the server should NOT return
+ // a refresh token for a client credentials grant.
+ if($grantType != 'refresh_token' && property_exists($json, 'refresh_token')) {
+ $data['refresh_token'] = $json->refresh_token;
+ }
+
+ // If the Oauth2 server returned `expires_in` use it to set the
+ // access token expiration time. See section 5.1 of RFC 6749.
+ if(property_exists($json, 'expires_in')) {
+ $data['access_token_exp'] = time() + $json->expires_in;
+ }
+
+ // Update the dataset
+ $srvr = $this->patchEntity($srvr, $data);
+ if (!$this->save($srvr)) {
+ throw new \RuntimeException(__d('error', 'save' [__d('core_server', 'field.Oauth2Servers.access_token')]));
+ }
+ }
+
+ return $json;
+ }
+
+
+ /**
+ * Generate a redirect URI for the given server ID.
+ *
+ * @param int|string $id The unique identifier of the OAuth2 server
+ * @return string The full URL of the redirect URI
+ */
+ public function redirectUri(int|string $id): string
+ {
+ $callback = [
+ 'plugin' => 'CoreServer',
+ 'controller' => 'Oauth2Servers',
+ 'action' => 'callback',
+ $id
+ ];
+
+ return Router::url($callback, true);
+ }
+
+
+ /**
+ * Refresh the OAuth access token using the stored refresh token.
+ *
+ * @param int|string $id The unique identifier of the OAuth2 server
+ * @return string The new access token
+ * @throws RuntimeException
+ * @since COmanage Registry v5.2.0
+ */
+ public function refreshToken(int|string $id):string
+ {
+ $json = $this->obtainToken((int)$id, 'refresh_token');
+
+ return $json->access_token;
+ }
+
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('server_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('server_id');
+
+ $validator->add('access_grant_type', [
+ 'content' => ['rule' => ['inList', GrantTypesEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('access_grant_type');
+
+ $validator->add('url', ['content' => ['rule' => 'url']]);
+ $validator->notEmptyString('url');
+
+ $validator->notEmptyString('clientid');
+ $validator->notEmptyString('client_secret');
+
+ $validator->add('scope', [
+ 'content' => [
+ 'rule' => 'validateNotBlank',
+ 'provider' => 'table'
+ ]
+ ]);
+ $validator->notEmptyString('scope');
+
+ $validator->add('refresh_token', [
+ 'content' => [
+ 'rule' => 'validateNotBlank',
+ 'provider' => 'table'
+ ]
+ ]);
+ $validator->allowEmptyString('refresh_token');
+
+ $validator->add('access_token', [
+ 'content' => [
+ 'rule' => 'validateNotBlank',
+ 'provider' => 'table'
+ ]
+ ]);
+ $validator->allowEmptyString('access_token');
+
+ $validator->add('token_response', [
+ 'content' => [
+ 'rule' => 'validateNotBlank',
+ 'provider' => 'table'
+ ]
+ ]);
+ $validator->allowEmptyString('token_response');
+
+ $validator->integer('access_token_exp')
+ ->allowEmptyString('access_token_exp');
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php b/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php
index 8030d5d4c..7e14e836b 100644
--- a/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php
+++ b/app/plugins/CoreServer/src/Model/Table/SqlServersTable.php
@@ -36,6 +36,7 @@
use Cake\ORM\Table;
use Cake\Validation\Validator;
use CoreServer\Lib\Enum\RdbmsTypeEnum;
+use \App\Lib\Enum\SuspendableStatusEnum;
class SqlServersTable extends Table {
use \App\Lib\Traits\AutoViewVarsTrait;
@@ -127,8 +128,12 @@ public function connect(int $serverId, string $name): bool {
// which is basically what the SQL Provisioner does.
// Pull our configuration via the parent Server object.
- $server = $this->Servers->get($serverId, ['contain' => ['SqlServers']]);
+ $server = $this->Servers->get($serverId, contain: ['SqlServers']);
+ if($server->status != SuspendableStatusEnum::Active) {
+ throw new \InvalidArgumentException(__d('error', 'inactive', [__d('controller', 'Servers', [1]), $serverId]));
+ }
+
$dbmap = [
RdbmsTypeEnum::MariaDB => 'Mysql',
RdbmsTypeEnum::MySQL => 'Mysql',
@@ -168,15 +173,17 @@ public function connect(int $serverId, string $name): bool {
// Use 'CakeDC\\OracleDriver\\Database\\Driver\\OraclePDO' for PDO_OCI, but CakeDC
// recommends OCI8
- // The plugin documentation says certain features are enabled at v12, so we hard
- // code that version to simplify configuration. As of this writing, Oracle 11g
- // is the oldest supported version, but 12c dates back to July 2013, so it seems
- // reasonable to require v12 (at least for now). Note Oracle changed their release
- // numbers to be based on calendar years, retroactively assigning 18c (12.2.0.2)
- // and 19c (12.2.0.3), so this approach should work at least for those versions.
- // Since semantic versioning is not being used, it's unclear when backwards
- // incompatible changes might be introduced, or if the CakeDC plugin even cares.
- $dbconfig['server_version'] = 12;
+ // The plugin documentation says certain features are enabled at v12, and more
+ // specifically we require support for long aliases (Oracle only supported 30
+ // characters until 12.2, which allows 128). See eg this commit
+ // https://github.com/CakeDC/cakephp-oracle-driver/pull/57/commits/1461451ce896aa55a14b08fddc0b28266a3391df
+ // Oracle 19c (aka 19.1.0 aka 12.2.0.3) appears to be the current oldest release
+ // (as of this writing), and based on testing from SMU setting this value to "19"
+ // correctly enables the long alias support, to we hard code that version to simplify
+ // configuration. Note Oracle changed their release numbers to be based on calendar years,
+ // retroactively assigning 18c (12.2.0.2) and 19c (12.2.0.3), so this approach should
+ // work at least for those versions.
+ $dbconfig['server_version'] = 19;
}
}
diff --git a/app/plugins/CoreServer/src/config/plugin.json b/app/plugins/CoreServer/src/config/plugin.json
deleted file mode 100644
index a1901134c..000000000
--- a/app/plugins/CoreServer/src/config/plugin.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
- "types": {
- "server": [
- "HttpServers",
- "MatchServers",
- "SmtpServers",
- "SqlServers"
- ]
- },
- "schema": {
- "tables": {
- "http_servers": {
- "columns": {
- "id": {},
- "server_id": {},
- "url": { "type": "url" },
- "username": {},
- "password": {},
- "auth_type": { "type": "string", "size": 2 },
- "skip_ssl_verification": { "type": "boolean" }
- },
- "indexes": {
- "http_servers_i1": { "columns": [ "server_id" ]}
- }
- },
- "match_servers": {
- "columns": {
- "id": {},
- "server_id": {},
- "url": { "type": "url" },
- "username": {},
- "password": {},
- "auth_type": { "type": "string", "size": 2 },
- "skip_ssl_verification": { "type": "boolean" }
- },
- "indexes": {
- "match_servers_i1": { "columns": [ "server_id" ] }
- }
- },
- "smtp_servers": {
- "columns": {
- "id": {},
- "server_id": {},
- "hostname": { "type": "string", "size": 128 },
- "port": { "type": "integer" },
- "username": {},
- "password": {},
- "use_tls": { "type": "boolean" },
- "default_from": { "type": "string", "size": 256 },
- "default_reply_to": { "type": "string", "size": 256 },
- "override_to": { "type": "string", "size": 256 }
- },
- "indexes": {
- "smtp_servers_i1": { "columns": [ "server_id" ] }
- }
- },
- "sql_servers": {
- "columns": {
- "id": {},
- "server_id": {},
- "type": { "type": "string", "size": 2 },
- "hostname": { "type": "string", "size": 128 },
- "port": { "type": "integer" },
- "databas": { "type": "string", "size": 128 },
- "username": {},
- "password": {}
- },
- "indexes": {
- "sql_servers_i1": { "columns": [ "server_id" ]}
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/plugins/CoreServer/templates/HttpServers/fields.inc b/app/plugins/CoreServer/templates/HttpServers/fields.inc
index d643222b2..6926478fc 100644
--- a/app/plugins/CoreServer/templates/HttpServers/fields.inc
+++ b/app/plugins/CoreServer/templates/HttpServers/fields.inc
@@ -25,18 +25,18 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
-// This view does currently not support read-only, and add is not used
-if($vv_action == 'edit') {
- foreach ([
- 'url',
- 'username',
- 'password',
- 'auth_type',
- 'skip_ssl_verification'
- ] as $field) {
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => $field
- ]]);
- }
-}
+$fields = [
+ 'url',
+ 'username',
+ 'password',
+ 'auth_type',
+ 'skip_ssl_verification'
+];
+
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.HttpServers'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.HttpServers' => ['edit']
+ ]
+];
diff --git a/app/plugins/CoreServer/templates/MatchServerAttributes/columns.inc b/app/plugins/CoreServer/templates/MatchServerAttributes/columns.inc
new file mode 100644
index 000000000..2e2d71ad4
--- /dev/null
+++ b/app/plugins/CoreServer/templates/MatchServerAttributes/columns.inc
@@ -0,0 +1,52 @@
+ [
+ 'type' => 'link'
+ ],
+ 'type_id' => [
+ 'type' => 'fk'
+ ],
+ 'required' => [
+ 'type' => 'enum',
+ 'class' => 'RequiredEnum'
+ ]
+];
+
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.MatchServers', 'CoreServer.MatchServerAttributes'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.MatchServers' => ['edit'],
+ 'CoreServer.MatchServerAttributes' => ['index']
+ ]
+];
diff --git a/app/plugins/CoreServer/templates/MatchServerAttributes/fields.inc b/app/plugins/CoreServer/templates/MatchServerAttributes/fields.inc
new file mode 100644
index 000000000..d674bc5e8
--- /dev/null
+++ b/app/plugins/CoreServer/templates/MatchServerAttributes/fields.inc
@@ -0,0 +1,150 @@
+ [ // type_id is a hidden field used to persist the type
+ 'type' => 'integer'
+ ]
+];
+
+// These are display widgets that are filtered to the correct available types
+foreach ([
+ 'address_type_id',
+ 'email_address_type_id',
+ 'identifier_type_id',
+ 'name_type_id',
+ 'telephone_number_type_id'
+] as $field) {
+ $fields[$field] = [
+ 'fieldLabel' => __d('field', 'type')
+ ];
+}
+
+$fields[] = 'required';
+
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.MatchServers', 'CoreServer.MatchServerAttributes'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.MatchServers' => ['edit'],
+ 'CoreServer.MatchServerAttributes' => ['index']
+ ]
+];
+?>
+
+
diff --git a/app/plugins/CoreServer/templates/MatchServers/fields.inc b/app/plugins/CoreServer/templates/MatchServers/fields.inc
index 550143398..8fb6ac14c 100644
--- a/app/plugins/CoreServer/templates/MatchServers/fields.inc
+++ b/app/plugins/CoreServer/templates/MatchServers/fields.inc
@@ -28,13 +28,29 @@
// At least initially we use only the HttpServer fields
include ROOT . DS . "plugins" . DS . "CoreServer" . DS . "templates" . DS . "HttpServers" . DS . "fields.inc";
-// Render the callback API endpoint
+// If the Match Server URL is empty, then we're in the initial configuration, so we hide the
+// Match Server Attributes link so the admin doesn't try to visit that link without finishing
+// the main configuration
+if(!empty($vv_obj->url)) {
+ $topLinks[] = [
+ 'icon' => 'list',
+ 'order' => 'Default',
+ 'label' => __d('core_server', 'controller.MatchServerAttributes', [99]),
+ 'link' => [
+ 'plugin' => 'CoreServer',
+ 'controller' => 'match_server_attributes',
+ 'action' => 'index',
+ 'match_server_id' => $vv_obj->id
+ ],
+ 'class' => ''
+ ];
+}
-print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'api_endpoint',
- 'fieldOptions' => [
- 'readOnly' => true,
- 'default' => $vv_api_endpoint
- ]
- ]]);
\ No newline at end of file
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.MatchServers', 'CoreServer.MatchServerAttributes'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.MatchServers' => ['edit'],
+ 'CoreServer.MatchServerAttributes' => ['index']
+ ]
+];
diff --git a/app/plugins/CoreServer/templates/Oauth2Servers/fields.inc b/app/plugins/CoreServer/templates/Oauth2Servers/fields.inc
new file mode 100644
index 000000000..aa342d45b
--- /dev/null
+++ b/app/plugins/CoreServer/templates/Oauth2Servers/fields.inc
@@ -0,0 +1,74 @@
+ [
+ 'readonly' => true,
+ 'default' => $vv_redirect_uri
+ ],
+ 'url',
+ 'access_grant_type' => [
+ 'options' => $types,
+ 'type' => 'select',
+ 'empty' => false
+ ],
+ 'clientid',
+ 'client_secret',
+ 'scope' => [
+ 'placeholder' => '/authenticate',
+ ]
+];
+
+$generateLink = [];
+if(!empty($vv_obj->id)) {
+ $generateLink = [
+ 'url' => [
+ 'plugin' => 'CoreServer',
+ 'controller' => 'Oauth2Servers',
+ 'action' => 'token',
+ $vv_obj->id
+ ],
+ 'label' => __d('core_server', 'info.Oauth2Servers.token.obtain'),
+ 'class' => 'provisionbutton nospin btn btn-primary btn-sm',
+ ];
+}
+
+$fields['access_token'] = [
+ 'status' => !empty($vv_obj->access_token) ? __d('enumeration', 'SetBooleanEnum.1') : __d('enumeration', 'SetBooleanEnum.0'),
+ 'link' => $generateLink,
+ 'labelIsTextOnly' => true
+];
+
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.Oauth2Servers'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.Oauth2Servers' => ['edit']
+ ]
+];
+
\ No newline at end of file
diff --git a/app/plugins/CoreServer/templates/SmtpServers/fields.inc b/app/plugins/CoreServer/templates/SmtpServers/fields.inc
index bac623cdc..882000297 100644
--- a/app/plugins/CoreServer/templates/SmtpServers/fields.inc
+++ b/app/plugins/CoreServer/templates/SmtpServers/fields.inc
@@ -25,50 +25,26 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
-// This view does currently not support read-only, and add is not used
-if($vv_action == 'edit') {
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'hostname',
- ]
- ]);
+$fields = [
+ 'hostname',
+ 'port' => [
+ 'default' => 587
+ ],
+ 'username',
+ 'password',
+ 'use_tls' => [
+ 'default' => true
+ ],
+ 'default_from',
+ 'default_reply_to',
+ 'override_to'
+];
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'port',
- 'fieldOptions' => [
- 'default' => 587
- ]
- ]]);
-
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'username',
- ]
- ]);
-
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'password',
- ]
- ]);
-
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'use_tls',
- 'fieldOptions' => [
- 'default' => true
- ]
- ]]);
-
- foreach ([
- 'default_from',
- 'default_reply_to',
- 'override_to',
- ] as $field) {
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => $field
- ]]);
- }
-}
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.SmtpServers'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.SmtpServers' => ['edit']
+ ]
+];
+
\ No newline at end of file
diff --git a/app/plugins/CoreServer/templates/SqlServers/fields.inc b/app/plugins/CoreServer/templates/SqlServers/fields.inc
index a15d291f9..7b9482170 100644
--- a/app/plugins/CoreServer/templates/SqlServers/fields.inc
+++ b/app/plugins/CoreServer/templates/SqlServers/fields.inc
@@ -25,20 +25,19 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
-// This view does currently not support read-only, and add is not used
-if($vv_action == 'edit') {
- foreach([
- 'type',
- 'hostname',
- 'port',
- 'databas',
- 'username',
- 'password',
- ] as $field) {
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => $field,
- ]
- ]);
- }
-}
+$fields = [
+ 'type',
+ 'hostname',
+ 'port',
+ 'databas',
+ 'username',
+ 'password'
+];
+
+$subnav = [
+ 'tabs' => ['Servers', 'CoreServer.SqlServers'],
+ 'action' => [
+ 'Servers' => ['edit'],
+ 'CoreServer.SqlServers' => ['edit']
+ ]
+];
diff --git a/app/plugins/EnvSource/config/plugin.json b/app/plugins/EnvSource/config/plugin.json
new file mode 100644
index 000000000..91680f5ea
--- /dev/null
+++ b/app/plugins/EnvSource/config/plugin.json
@@ -0,0 +1,106 @@
+{
+ "types": {
+ "enrollment_flow_step": [
+ "EnvSourceCollectors"
+ ],
+ "external_identity_source": [
+ "EnvSources"
+ ],
+ "traffic_detour": [
+ "EnvSourceDetours"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "env_source_collectors": {
+ "columns": {
+ "id": {},
+ "enrollment_flow_step_id": {},
+ "external_identity_source_id": {},
+ "enable_confirmation_page": { "type": "boolean" }
+ },
+ "indexes": {
+ "env_source_collectors_i1": { "columns": [ "enrollment_flow_step_id" ] },
+ "env_source_collectors_i2": { "needed": false, "columns": [ "external_identity_source_id" ] }
+ }
+ },
+ "env_sources": {
+ "columns": {
+ "id": {},
+ "external_identity_source_id": {},
+ "redirect_on_duplicate": { "type": "url" },
+ "mva_delimiter": { "type": "string", "size": "1" },
+ "sync_on_login": { "type": "boolean"},
+ "default_affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "email_address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "name_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "telephone_number_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "env_address_street": { "type": "string", "size": "80" },
+ "env_address_locality": { "type": "string", "size": "80" },
+ "env_address_state": { "type": "string", "size": "80" },
+ "env_address_postalcode": { "type": "string", "size": "80" },
+ "env_address_country": { "type": "string", "size": "80" },
+ "env_affiliation": { "type": "string", "size": "80" },
+ "env_department": { "type": "string", "size": "80" },
+ "env_identifier_eppn": { "type": "string", "size": "80" },
+ "env_identifier_eptid": { "type": "string", "size": "80" },
+ "env_identifier_epuid": { "type": "string", "size": "80" },
+ "env_identifier_network": { "type": "string", "size": "80" },
+ "env_identifier_oidcsub": { "type": "string", "size": "80" },
+ "env_identifier_samlpairwiseid": { "type": "string", "size": "80" },
+ "env_identifier_samlsubjectid": { "type": "string", "size": "80" },
+ "env_identifier_sourcekey": { "type": "string", "size": "80" },
+ "env_mail": { "type": "string", "size": "80" },
+ "env_name_honorific": { "type": "string", "size": "80" },
+ "env_name_given": { "type": "string", "size": "80" },
+ "env_name_middle": { "type": "string", "size": "80" },
+ "env_name_family": { "type": "string", "size": "80" },
+ "env_name_suffix": { "type": "string", "size": "80" },
+ "env_organization": { "type": "string", "size": "80" },
+ "env_telephone_number": { "type": "string", "size": "80" },
+ "env_title": { "type": "string", "size": "80" },
+ "lookaside_file": { "type": "path" }
+ },
+ "indexes": {
+ "env_sources_i1": { "columns": [ "external_identity_source_id" ] }
+ }
+ },
+ "env_source_identities": {
+ "columns": {
+ "id": {},
+ "env_source_id": { "type": "integer", "foreignkey": { "table": "env_sources", "column": "id" }},
+ "source_key": { "type": "string", "size": 1024 },
+ "env_attributes": { "type": "text" }
+ },
+ "indexes": {
+ "env_source_identities_i1": { "columns": [ "source_key" ] },
+ "env_source_identities_i2": { "needed": false, "columns": [ "env_source_id" ] }
+ },
+ "clone_relation": true
+ },
+ "env_source_detours": {
+ "columns": {
+ "id": {},
+ "traffic_detour_id": {}
+ },
+ "indexes": {
+ "env_source_detours_i1": { "needed": false, "columns": [ "traffic_detour_id" ] }
+ }
+ },
+ "petition_env_identities": {
+ "columns": {
+ "id": {},
+ "petition_id": {},
+ "env_source_collector_id": { "type": "integer", "foreignkey": { "table": "env_source_collectors", "column": "id" }},
+ "env_source_identity_id": { "type": "integer", "foreignkey": { "table": "env_source_identities", "column": "id" }}
+ },
+ "indexes": {
+ "petition_env_identities_i1": { "columns": [ "petition_id" ] },
+ "petition_env_identities_i2": { "needed": false, "columns": [ "env_source_collector_id" ] },
+ "petition_env_identities_i3": { "needed": false, "columns": [ "env_source_identity_id" ] }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/EnvSource/resources/locales/en_US/env_source.po b/app/plugins/EnvSource/resources/locales/en_US/env_source.po
index 2cf309708..d0c39ad61 100644
--- a/app/plugins/EnvSource/resources/locales/en_US/env_source.po
+++ b/app/plugins/EnvSource/resources/locales/en_US/env_source.po
@@ -31,6 +31,9 @@ msgstr "{0,plural,=1{Env Source} other{Env Sources}}"
msgid "controller.PetitionEnvIdentities"
msgstr "{0,plural,=1{Petition Env Identity} other{Petition Env Identities}}"
+msgid "display.EnvSource"
+msgstr "{0} Source"
+
msgid "enumeration.EnvSourceSpModeEnum.O"
msgstr "Other"
@@ -49,9 +52,18 @@ msgstr "Source Key (env_identifier_sourcekey) not found in attributes"
msgid "error.source_key.duplicate"
msgstr "Source Key {0} is already attached to External Identity {1}"
+msgid "field.EnvSourceCollectors.enable_confirmation_page"
+msgstr "Enable Confirmation Page"
+
+msgid "field.EnvSourceCollectors.enable_confirmation_page.desc"
+msgstr "If enabled, Enrollees will be presented with the values asserted by their IdP to review before continuing"
+
msgid "field.EnvSources.address_type_id"
msgstr "Address Type"
+msgid "field.EnvSources.mva_delimiter_common"
+msgstr "Common Delimiters"
+
msgid "field.EnvSources.default_affiliation_type_id"
msgstr "Default Affiliation Type"
@@ -142,8 +154,8 @@ msgstr "Path to lookaside file, intended for testing only"
msgid "field.EnvSources.redirect_on_duplicate"
msgstr "Redirect on Duplicate"
-msgid "field.EnvSources.sp_mode"
-msgstr "Web Server Service Provider"
+msgid "field.EnvSources.mva_delimiter"
+msgstr "Service Provider Multi Value Delimiter"
msgid "field.EnvSources.sync_on_login"
msgstr "Sync on Login"
@@ -170,4 +182,4 @@ msgid "result.env.saved.login"
msgstr "Env Attributes updated at login"
msgid "result.pipeline.status"
-msgstr "Pipeline completed with status {0}"
\ No newline at end of file
+msgstr "EnvSource Pipeline completed with status {0}"
\ No newline at end of file
diff --git a/app/plugins/EnvSource/src/Controller/EnvSourceCollectorsController.php b/app/plugins/EnvSource/src/Controller/EnvSourceCollectorsController.php
index 167b8ef22..dcba7ae83 100644
--- a/app/plugins/EnvSource/src/Controller/EnvSourceCollectorsController.php
+++ b/app/plugins/EnvSource/src/Controller/EnvSourceCollectorsController.php
@@ -36,7 +36,7 @@
use Cake\ORM\TableRegistry;
class EnvSourceCollectorsController extends StandardEnrollerController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'EnvSourceCollectors.id' => 'asc'
]
@@ -78,14 +78,15 @@ public function dispatch(string $id) {
// Pull our configuration
- $envSource = $this->EnvSourceCollectors->get((int)$id, ['contain' => ['ExternalIdentitySources' => 'EnvSources']]);
+ $envSource = $this->EnvSourceCollectors->get((int)$id, contain: ['ExternalIdentitySources' => 'EnvSources']);
try {
$vars = $this->EnvSourceCollectors->parse($envSource->external_identity_source->env_source);
$this->set('vv_env_source_vars', $vars);
- if($this->request->is(['post', 'put'])) {
+ if($this->request->is(['post', 'put'])
+ || ($this->request->is(['get']) && !$envSource->enable_confirmation_page)) {
// We'll upsert the collected attributes. Generally this should always be an insert,
// but we could imagine a scenario where an admin reruns the step to change the
// collected identity. Or maybe if the enrollee just hits the back button.
diff --git a/app/plugins/EnvSource/src/Controller/EnvSourceDetoursController.php b/app/plugins/EnvSource/src/Controller/EnvSourceDetoursController.php
index dbd5ffa7b..55d90b3f6 100644
--- a/app/plugins/EnvSource/src/Controller/EnvSourceDetoursController.php
+++ b/app/plugins/EnvSource/src/Controller/EnvSourceDetoursController.php
@@ -35,7 +35,7 @@
use \App\Lib\Events\CoIdEventListener;
class EnvSourceDetoursController extends StandardDetourController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'EnvSourceDetours.id' => 'asc'
]
diff --git a/app/plugins/EnvSource/src/Controller/EnvSourcesController.php b/app/plugins/EnvSource/src/Controller/EnvSourcesController.php
index 81d6dd222..40cd534aa 100644
--- a/app/plugins/EnvSource/src/Controller/EnvSourcesController.php
+++ b/app/plugins/EnvSource/src/Controller/EnvSourcesController.php
@@ -34,7 +34,7 @@
use Cake\Http\Response;
class EnvSourcesController extends StandardPluginController {
- public $paginate = [
+ protected array $paginate = [
'order' => [
'EnvSources.id' => 'asc'
]
diff --git a/app/plugins/EnvSource/src/Model/Entity/EnvSource.php b/app/plugins/EnvSource/src/Model/Entity/EnvSource.php
index a739b2d30..49d121a1d 100644
--- a/app/plugins/EnvSource/src/Model/Entity/EnvSource.php
+++ b/app/plugins/EnvSource/src/Model/Entity/EnvSource.php
@@ -32,6 +32,8 @@
use Cake\ORM\Entity;
class EnvSource extends Entity {
+ use \App\Lib\Traits\EntityMetaTrait;
+
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -41,7 +43,7 @@ class EnvSource extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/EnvSource/src/Model/Entity/EnvSourceCollector.php b/app/plugins/EnvSource/src/Model/Entity/EnvSourceCollector.php
index 91989da2c..3b8099365 100644
--- a/app/plugins/EnvSource/src/Model/Entity/EnvSourceCollector.php
+++ b/app/plugins/EnvSource/src/Model/Entity/EnvSourceCollector.php
@@ -43,7 +43,7 @@ class EnvSourceCollector extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/EnvSource/src/Model/Entity/EnvSourceDetour.php b/app/plugins/EnvSource/src/Model/Entity/EnvSourceDetour.php
index 5b8717dc7..a73a1ebbd 100644
--- a/app/plugins/EnvSource/src/Model/Entity/EnvSourceDetour.php
+++ b/app/plugins/EnvSource/src/Model/Entity/EnvSourceDetour.php
@@ -43,7 +43,7 @@ class EnvSourceDetour extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/EnvSource/src/Model/Entity/EnvSourceIdentity.php b/app/plugins/EnvSource/src/Model/Entity/EnvSourceIdentity.php
index 2873ca37c..bbab90dd5 100644
--- a/app/plugins/EnvSource/src/Model/Entity/EnvSourceIdentity.php
+++ b/app/plugins/EnvSource/src/Model/Entity/EnvSourceIdentity.php
@@ -32,6 +32,8 @@
use Cake\ORM\Entity;
class EnvSourceIdentity extends Entity {
+ use \App\Lib\Traits\EntityMetaTrait;
+
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -41,7 +43,7 @@ class EnvSourceIdentity extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/EnvSource/src/Model/Entity/PetitionEnvIdentity.php b/app/plugins/EnvSource/src/Model/Entity/PetitionEnvIdentity.php
index beb919bfc..684db2846 100644
--- a/app/plugins/EnvSource/src/Model/Entity/PetitionEnvIdentity.php
+++ b/app/plugins/EnvSource/src/Model/Entity/PetitionEnvIdentity.php
@@ -32,6 +32,8 @@
use Cake\ORM\Entity;
class PetitionEnvIdentity extends Entity {
+ use \App\Lib\Traits\EntityMetaTrait;
+
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -41,7 +43,7 @@ class PetitionEnvIdentity extends Entity {
*
* @var array
*/
- protected $_accessible = [
+ protected array $_accessible = [
'*' => true,
'id' => false,
'slug' => false,
diff --git a/app/plugins/EnvSource/src/Model/Table/EnvSourceCollectorsTable.php b/app/plugins/EnvSource/src/Model/Table/EnvSourceCollectorsTable.php
index 5a865dbfe..f6870577f 100644
--- a/app/plugins/EnvSource/src/Model/Table/EnvSourceCollectorsTable.php
+++ b/app/plugins/EnvSource/src/Model/Table/EnvSourceCollectorsTable.php
@@ -31,6 +31,7 @@
use Cake\Datasource\ConnectionManager;
use Cake\Datasource\EntityInterface;
+use Cake\Datasource\Exception\RecordNotFoundException;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
@@ -47,7 +48,6 @@ class EnvSourceCollectorsTable extends Table {
use \App\Lib\Traits\PrimaryLinkTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
- use \App\Lib\Traits\TabTrait;
/**
* Perform Cake Model initialization.
@@ -80,27 +80,10 @@ public function initialize(array $config): void {
$this->setRequiresCO(true);
$this->setAllowLookupPrimaryLink(['dispatch', 'display']);
- // All the tabs share the same configuration in the ModelTable file
- $this->setTabsConfig(
- [
- // Ordered list of Tabs
- 'tabs' => ['EnrollmentFlowSteps', 'EnvSource.EnvSourceCollectors'],
- // What actions will include the subnavigation header
- 'action' => [
- // If a model renders in a subnavigation mode in edit/view mode, it cannot
- // render in index mode for the same use case/context
- // XXX edit should go first.
- 'EnrollmentFlowSteps' => ['edit', 'view'],
- 'EnvSource.EnvSourceCollectors' => ['edit'],
- ]
- ]
- );
-
$this->setAutoViewVars([
'externalIdentitySources' => [
- 'type' => 'select',
- 'model' => 'ExternalIdentitySources',
- 'where' => ['plugin' => 'EnvSource.EnvSources']
+ 'type' => 'plugin',
+ 'model' => 'EnvSource.EnvSources'
]
]);
@@ -150,6 +133,71 @@ protected function checkDuplicate(int $eisId, string $sourceKey): bool {
return true;
}
+ /**
+ * Obtain a name (as a string) for the Enrollee associated with the specified Petition.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param EntityInterface $config Configuration entity for this plugin
+ * @param int $petitionId Petition ID
+ * @return string Name, or null if thre is no name data
+ */
+
+ public function enrolleeName(
+ EntityInterface $config,
+ int $petitionId
+ ): ?string {
+ $ret = null;
+
+ // To find an Enrolle Name we have to piece together a number of different queries.
+ // First, look up our configuration to find the EnvSource configuration in use.
+
+ $EnvSources = TableRegistry::getTableLocator()->get('EnvSource.EnvSources');
+
+ $envcfg = $EnvSources->find()
+ ->where([
+ 'external_identity_source_id' => $config->external_identity_source_id
+ ])
+ ->firstOrFail();
+
+ // Now pull the EnvSourceIdentity. From here on out any failure just means we don't
+ // return a value (instead of throwing an exception).
+
+ $pei = $this->PetitionEnvIdentities
+ ->find()
+ ->where([
+ 'petition_id' => $petitionId,
+ 'env_source_collector_id' => $config->id
+ ])
+ ->contain(['EnvSourceIdentities'])
+ ->first();
+
+ if(!empty($pei->env_source_identity->env_attributes)) {
+ // Create an array of env source columns to stored values
+ $envattrs = json_decode(json: $pei->env_source_identity->env_attributes, associative: true);
+
+ // Build a Name entity with the mapped values. As a basic test, we'll require
+ // given name, otherwise we'll assume we don't have a valid name. Note we're not
+ // planning on saving the Name entity, we just create it via the table to ensure
+ // it is properly initialized.
+
+ if(!empty($envattrs['env_name_given'])) {
+ $Names = TableRegistry::getTableLocator()->get('Names');
+
+ $name = $Names->newEntity([
+ 'honorific' => $envattrs['env_name_honorific'] ?? null,
+ 'given' => $envattrs['env_name_given'] ?? null,
+ 'middle' => $envattrs['env_name_middle'] ?? null,
+ 'family' => $envattrs['env_name_family'] ?? null,
+ 'suffix' => $envattrs['env_name_suffix'] ?? null,
+ ]);
+
+ return $name->full_name;
+ }
+ }
+
+ return $ret;
+ }
+
/**
* Perform steps necessary to hydrate the Person record as part of Petition finalization.
*
@@ -206,6 +254,55 @@ public function hydrate(int $id, \App\Model\Entity\Petition $petition) {
action: PetitionActionEnum::Finalized,
comment: __d('env_source', 'result.pipeline.status', [$status])
);
+
+ // Because PetitionsTable::hydrate() creates a skeletal Person record without
+ // a Name, PipelinesTable::createPersonFromEIS() won't be called from sync(),
+ // so the Pipeline won't create a Primary Name if there isn't one already.
+ // As such, we need to check here if there is a Primary Name (highly dependent
+ // on the Flow configuration), and if there isn't one we'll create one (even
+ // though a subsequent step such as an Attribute Collector might create a new
+ // one later).
+
+ $Names = TableRegistry::getTableLocator()->get('Names');
+
+ try {
+ $Names->primaryName($petition->enrollee_person_id);
+ }
+ catch(RecordNotFoundException $e) {
+ // No Primary Name found, create one. Note we need to honor
+ // AR-Pipeline-1 If a Pipeline creates a new Person, the first Name
+ // returned by the External Identity Source backend will be used as
+ // the initial Primary Name for the new Person.
+ // so we call retrieve() for consistency (though note EnvSource only
+ // supports 1 name currently).
+
+ $EnvSources = TableRegistry::getTableLocator()->get('EnvSource.EnvSources');
+
+ $eis = $ExtIdentitySources->get(
+ $cfg->external_identity_source_id,
+ contain: 'EnvSources'
+ );
+
+ $eisrecord = $EnvSources->retrieve($eis, $pei->env_source_identity->source_key);
+
+ if(!empty($eisrecord['entity_data']['names'][0])) {
+ $name = $eisrecord['entity_data']['names'][0];
+
+ // Add the additional attributes and convert the type back to a type_id
+ $name['person_id'] = $petition->enrollee_person_id;
+ $name['primary_name'] = true;
+ $name['type_id'] = $eis->env_source->name_type_id;
+ unset($name['type']);
+
+ $Names->saveOrFail($Names->newEntity($name));
+
+ $this->llog('trace', 'EnvSource created Primary Name for Petition ' . $petition->id);
+ } else {
+ // This isn't necessarily an error since a subsequent step could create
+ // a Primary Name
+ $this->llog('trace', 'EnvSource could not find a Name for Petition ' . $petition->id);
+ }
+ }
}
catch(\Exception $e) {
// We allow an error in the sync process (probably a duplicate record) to interrupt
@@ -234,6 +331,41 @@ public function hydrate(int $id, \App\Model\Entity\Petition $petition) {
return true;
}
+ /**
+ * Load environment variables from a lookaside file based on the given configuration.
+ *
+ * @since COmanage Registry v5.1.0
+ * @param string $filename Path to the lookaside file
+ * @param \EnvSource\Model\Entity\EnvSource $envSource EnvSource configuration entity
+ * @return array Array of environment variables and their parsed values
+ * @throws InvalidArgumentException
+ */
+
+ public function loadFromLookasideFile(string $filename, \EnvSource\Model\Entity\EnvSource $envSource): array {
+ $src = parse_ini_file($filename);
+ $ret = [];
+
+ if(!$src) {
+ throw new \InvalidArgumentException(__d('env_source', 'error.lookaside_file', [$filename]));
+ }
+
+ // We walk through our configuration and only copy the variables that were configured
+ foreach($envSource->getVisible() as $field) {
+ // We only want the fields starting env_ (except env_source_id, which is changelog metadata)
+
+ if(strncmp($field, "env_", 4)==0 && $field != "env_source_id"
+ && !empty($envSource->$field) // This field is configured with an env var name
+ && isset($src[$envSource->$field]) // This env var is populated
+ ) {
+ // Note we're using the EnvSource field name (eg: env_name_given) as the key
+ // and not the configured variable name (which might be something like SHIB_FIRST_NAME)
+ $ret[$field] = $src[$envSource->$field];
+ }
+ }
+
+ return $ret;
+ }
+
/**
* Parse the environment values as per the configuration.
*
@@ -273,40 +405,6 @@ public function parse(\EnvSource\Model\Entity\EnvSource $envSource): array {
return $ret;
}
- /**
- * Load environment variables from a lookaside file based on the given configuration.
- *
- * @param string $filename Path to the lookaside file
- * @param \EnvSource\Model\Entity\EnvSource $envSource EnvSource configuration entity
- * @return array Array of environment variables and their parsed values
- * @throws InvalidArgumentException
- *@since COmanage Registry v5.1.0
- */
- public function loadFromLookasideFile(string $filename, \EnvSource\Model\Entity\EnvSource $envSource): array {
- $src = parse_ini_file($filename);
- $ret = [];
-
- if(!$src) {
- throw new \InvalidArgumentException(__d('env_source', 'error.lookaside_file', [$filename]));
- }
-
- // We walk through our configuration and only copy the variables that were configured
- foreach($envSource->getVisible() as $field) {
- // We only want the fields starting env_ (except env_source_id, which is changelog metadata)
-
- if(strncmp($field, "env_", 4)==0 && $field != "env_source_id"
- && !empty($envSource->$field) // This field is configured with an env var name
- && isset($src[$envSource->$field]) // This env var is populated
- ) {
- // Note we're using the EnvSource field name (eg: env_name_given) as the key
- // and not the configured variable name (which might be something like SHIB_FIRST_NAME)
- $ret[$field] = $src[$envSource->$field];
- }
- }
-
- return $ret;
- }
-
/**
* Insert or update a Petition Env Identity.
*
@@ -436,6 +534,11 @@ public function validationDefault(Validator $validator): Validator {
'content' => ['rule' => 'isInteger']
]);
$validator->notEmptyString('external_identity_source_id');
+
+ $validator->add('enable_confirmation_page', [
+ 'content' => ['rule' => ['boolean']]
+ ]);
+ $validator->allowEmptyString('enable_confirmation_page');
return $validator;
}
@@ -464,7 +567,7 @@ public function verifiableEmailAddresses(
$eis = $this->ExternalIdentitySources->get(
$config->external_identity_source_id,
- ['contain' => 'Pipelines']
+ contain: 'Pipelines'
);
$defaultVerified = isset($eis->pipeline->sync_verify_email_addresses)
diff --git a/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php b/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php
index 48e62f736..89b077107 100644
--- a/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php
+++ b/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php
@@ -40,7 +40,7 @@ class EnvSourcesTable extends Table {
use \App\Lib\Traits\LabeledLogTrait;
use \App\Lib\Traits\PermissionsTrait;
use \App\Lib\Traits\PrimaryLinkTrait;
- use \App\Lib\Traits\TabTrait;
+ use \App\Lib\Traits\QueryModificationTrait;
use \App\Lib\Traits\TableMetaTrait;
use \App\Lib\Traits\ValidationTrait;
@@ -97,22 +97,13 @@ public function initialize(array $config): void {
$this->setPrimaryLink(['external_identity_source_id']);
$this->setRequiresCO(true);
- // All the tabs share the same configuration in the ModelTable file
- $this->setTabsConfig(
- [
- // Ordered list of Tabs
- 'tabs' => ['ExternalIdentitySources', 'EnvSource.EnvSources', 'ExternalIdentitySources@action.search'],
- // What actions will include the subnavigation header
- 'action' => [
- // If a model renders in a subnavigation mode in edit/view mode, it cannot
- // render in index mode for the same use case/context
- // XXX edit should go first.
- 'ExternalIdentitySources' => ['edit', 'view', 'search'],
- 'EnvSource.EnvSources' => ['edit'],
- 'ExternalIdentitySources@action.search' => [],
- ],
- ]
- );
+ $this->setEditContains([
+ 'ExternalIdentitySources',
+ ]);
+
+ $this->setViewContains([
+ 'ExternalIdentitySources',
+ ]);
$this->setAutoViewVars([
'addressTypes' => [
@@ -156,6 +147,17 @@ public function initialize(array $config): void {
]);
}
+ /**
+ * Table specific logic to generate a display field.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param \EnvSource\Model\Entity\EnvSource $entity Entity to generate display field for
+ * @return string Display field
+ */
+ public function generateDisplayField(\EnvSource\Model\Entity\EnvSource $entity): string {
+ return __d('env_source', 'display.EnvSource', [$entity->external_identity_source->description]);
+ }
+
/**
* Obtain the set of changed records from the source database.
*
@@ -278,21 +280,15 @@ protected function resultToEntityData(
// Email Address
if(!empty($result['env_mail'])) {
- $mails = [];
-
// We accept multiple values if supported by the configured SP software.
-
- switch($EnvSource->sp_mode) {
- case EnvSourceSpModeEnum::Shibboleth:
- $mails = explode(";", $result['env_mail']);
- break;
- case EnvSourceSpModeEnum::SimpleSamlPhp:
- $mails = explode(",", $result['env_mail']);
- break;
- default:
- // We dont' try to tokenize the string
- $mails = [ $result['env_mail' ]];
- break;
+ $delimiter = $EnvSource->mva_delimiter;
+ $stringValue = $result['env_mail'];
+
+ // Check if the delimiter is provided and not an empty string
+ if (!empty($delimiter)) {
+ $mails = explode($delimiter, $stringValue);
+ } else {
+ $mails = [$stringValue];
}
foreach($mails as $m) {
@@ -315,8 +311,8 @@ protected function resultToEntityData(
'env_identifier_epuid' => 'epuid',
'env_identifier_network' => 'network',
'env_identifier_oidcsub' => 'oidcsub',
- 'env_identifier_samlpairwiseid' => 'samlpairwiseid',
- 'env_identifier_samlsubjectid' => 'samlsubjectid'
+ 'env_identifier_samlpairwiseid' => 'pairwiseid',
+ 'env_identifier_samlsubjectid' => 'subjectid'
// We don't include source_key (sorid) because the Pipeline will automatically insert it
] as $v => $t) {
// Because we're in an External Identity context, we don't need to map the
@@ -441,10 +437,7 @@ public function validationDefault(Validator $validator): Validator {
]);
$validator->allowEmptyString('redirect_on_duplicate');
- $validator->add('sp_mode', [
- 'content' => ['rule' => ['inList', EnvSourceSpModeEnum::getConstValues()]]
- ]);
- $validator->notEmptyString('sp_mode');
+ $this->registerStringValidation($validator, $schema, 'mva_delimiter', true);
$validator->add('sync_on_login', [
'content' => ['rule' => 'boolean']
diff --git a/app/plugins/EnvSource/src/View/Cell/EnvSourceCollectorsCell.php b/app/plugins/EnvSource/src/View/Cell/EnvSourceCollectorsCell.php
index 207b3c094..2e4e472e7 100644
--- a/app/plugins/EnvSource/src/View/Cell/EnvSourceCollectorsCell.php
+++ b/app/plugins/EnvSource/src/View/Cell/EnvSourceCollectorsCell.php
@@ -40,13 +40,28 @@
*/
class EnvSourceCollectorsCell extends Cell
{
+ /**
+ * @var mixed
+ */
+ public $vv_obj;
+
+ /**
+ * @var mixed
+ */
+ public $vv_step;
+
+ /**
+ * @var mixed
+ */
+ public $viewVars;
+
/**
* List of valid options that can be passed into this
* cell's constructor.
*
* @var array
*/
- protected $_validCellOptions = [
+ protected array $_validCellOptions = [
'vv_obj',
'vv_step',
'viewVars',
diff --git a/app/plugins/EnvSource/src/config/plugin.json b/app/plugins/EnvSource/src/config/plugin.json
deleted file mode 100644
index 0b8fd35bf..000000000
--- a/app/plugins/EnvSource/src/config/plugin.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "types": {
- "enroller": [
- "EnvSourceCollectors"
- ],
- "source": [
- "EnvSources"
- ],
- "traffic": [
- "EnvSourceDetours"
- ]
- },
- "schema": {
- "tables": {
- "env_source_collectors": {
- "columns": {
- "id": {},
- "enrollment_flow_step_id": {},
- "external_identity_source_id": {}
- },
- "indexes": {
- "env_source_collectors_i1": { "columns": [ "enrollment_flow_step_id" ] },
- "env_source_collectors_i2": { "needed": false, "columns": [ "external_identity_source_id" ] }
- }
- },
- "env_sources": {
- "columns": {
- "id": {},
- "external_identity_source_id": {},
- "redirect_on_duplicate": { "type": "url" },
- "sp_mode": { "type": "enum" },
- "sync_on_login": { "type": "boolean"},
- "default_affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
- "address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
- "email_address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
- "name_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
- "telephone_number_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
- "env_address_street": { "type": "string", "size": "80" },
- "env_address_locality": { "type": "string", "size": "80" },
- "env_address_state": { "type": "string", "size": "80" },
- "env_address_postalcode": { "type": "string", "size": "80" },
- "env_address_country": { "type": "string", "size": "80" },
- "env_affiliation": { "type": "string", "size": "80" },
- "env_department": { "type": "string", "size": "80" },
- "env_identifier_eppn": { "type": "string", "size": "80" },
- "env_identifier_eptid": { "type": "string", "size": "80" },
- "env_identifier_epuid": { "type": "string", "size": "80" },
- "env_identifier_network": { "type": "string", "size": "80" },
- "env_identifier_oidcsub": { "type": "string", "size": "80" },
- "env_identifier_samlpairwiseid": { "type": "string", "size": "80" },
- "env_identifier_samlsubjectid": { "type": "string", "size": "80" },
- "env_identifier_sourcekey": { "type": "string", "size": "80" },
- "env_mail": { "type": "string", "size": "80" },
- "env_name_honorific": { "type": "string", "size": "80" },
- "env_name_given": { "type": "string", "size": "80" },
- "env_name_middle": { "type": "string", "size": "80" },
- "env_name_family": { "type": "string", "size": "80" },
- "env_name_suffix": { "type": "string", "size": "80" },
- "env_organization": { "type": "string", "size": "80" },
- "env_telephone_number": { "type": "string", "size": "80" },
- "env_title": { "type": "string", "size": "80" },
- "lookaside_file": { "type": "path" }
- },
- "indexes": {
- "env_sources_i1": { "columns": [ "external_identity_source_id" ] }
- }
- },
- "env_source_identities": {
- "columns": {
- "id": {},
- "env_source_id": { "type": "integer", "foreignkey": { "table": "env_sources", "column": "id" }},
- "source_key": { "type": "string", "size": 1024 },
- "env_attributes": { "type": "text" }
- },
- "indexes": {
- "env_source_identities_i1": { "columns": [ "source_key" ] },
- "env_source_identities_i2": { "needed": false, "columns": [ "env_source_id" ] }
- }
- },
- "env_source_detours": {
- "columns": {
- "id": {},
- "traffic_detour_id": {}
- },
- "indexes": {
- "env_source_detours_i1": { "needed": false, "columns": [ "traffic_detour_id" ] }
- }
- },
- "petition_env_identities": {
- "columns": {
- "id": {},
- "petition_id": {},
- "env_source_collector_id": { "type": "integer", "foreignkey": { "table": "env_source_collectors", "column": "id" }},
- "env_source_identity_id": { "type": "integer", "foreignkey": { "table": "env_source_identities", "column": "id" }}
- },
- "indexes": {
- "petition_env_identities_i1": { "columns": [ "petition_id" ] },
- "petition_env_identities_i2": { "needed": false, "columns": [ "env_source_collector_id" ] },
- "petition_env_identities_i3": { "needed": false, "columns": [ "env_source_identity_id" ] }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/plugins/EnvSource/templates/EnvSourceCollectors/dispatch.inc b/app/plugins/EnvSource/templates/EnvSourceCollectors/dispatch.inc
index ef474132b..9c0faa879 100644
--- a/app/plugins/EnvSource/templates/EnvSourceCollectors/dispatch.inc
+++ b/app/plugins/EnvSource/templates/EnvSourceCollectors/dispatch.inc
@@ -29,7 +29,7 @@ declare(strict_types = 1);
// This view is intended to work with dispatch
if($vv_action == 'dispatch') {
- // Make the Form fields editable
+ // This form isn't editable, but we need the continue button to render
$this->Field->enableFormEditMode();
ksort($vv_env_source_vars);
$previousKey = '';
diff --git a/app/plugins/EnvSource/templates/EnvSourceCollectors/fields.inc b/app/plugins/EnvSource/templates/EnvSourceCollectors/fields.inc
index 713b393ba..e302021a4 100644
--- a/app/plugins/EnvSource/templates/EnvSourceCollectors/fields.inc
+++ b/app/plugins/EnvSource/templates/EnvSourceCollectors/fields.inc
@@ -25,12 +25,17 @@
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
-// This view only supports edit
-if($vv_action == 'edit') {
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => 'external_identity_source_id',
- 'fieldLabel' => __d('env_source', 'controller.EnvSources', [1])
- ]
- ]);
-}
+$fields = [
+ 'external_identity_source_id' => [
+ 'fieldLabel' => __d('env_source', 'controller.EnvSources', [1])
+ ],
+ 'enable_confirmation_page'
+];
+
+$subnav = [
+ 'tabs' => ['EnrollmentFlowSteps', 'EnvSource.EnvSourceCollectors'],
+ 'action' => [
+ 'EnrollmentFlowSteps' => ['edit', 'view'],
+ 'EnvSource.EnvSourceCollectors' => ['edit']
+ ],
+];
diff --git a/app/plugins/EnvSource/templates/EnvSources/fields-nav.inc b/app/plugins/EnvSource/templates/EnvSources/fields-nav.inc
deleted file mode 100644
index e174c7b99..000000000
--- a/app/plugins/EnvSource/templates/EnvSources/fields-nav.inc
+++ /dev/null
@@ -1,31 +0,0 @@
- 'plugin',
- 'active' => 'plugin'
- ];
\ No newline at end of file
diff --git a/app/plugins/EnvSource/templates/EnvSources/fields.inc b/app/plugins/EnvSource/templates/EnvSources/fields.inc
index 54fb0f882..5bcbf00bc 100644
--- a/app/plugins/EnvSource/templates/EnvSources/fields.inc
+++ b/app/plugins/EnvSource/templates/EnvSources/fields.inc
@@ -24,92 +24,150 @@
* @since COmanage Registry v5.1.0
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
-?>
-element('form/listItem', [
- 'arguments' => [
- 'fieldName' => $field
- ]
- ]);
+use EnvSource\Lib\Enum\EnvSourceSpModeEnum;
+
+// Build the delimiter map dynamically from your $spModes array
+$phpDelimiterMap = [];
+$selectedValue = '';
+foreach ($spModes as $key => $label) {
+ if ($key === EnvSourceSpModeEnum::Shibboleth) {
+ $phpDelimiterMap[$key] = ';';
+ if ($vv_obj?->mva_delimiter == ';') {
+ $selectedValue = EnvSourceSpModeEnum::Shibboleth;
+ }
+ } elseif ($key === EnvSourceSpModeEnum::SimpleSamlPhp) {
+ $phpDelimiterMap[$key] = ',';
+ if ($vv_obj?->mva_delimiter == ',') {
+ $selectedValue = EnvSourceSpModeEnum::SimpleSamlPhp;
+ }
+ } elseif ($key === EnvSourceSpModeEnum::Other) {
+ $phpDelimiterMap[$key] = '';
+ if (!in_array($vv_obj?->mva_delimiter, ['', ',', ';'])) {
+ $phpDelimiterMap[$key] = $vv_obj?->mva_delimiter;
+ $selectedValue = EnvSourceSpModeEnum::Other;
+ }
}
+}
- print "" . __d('env_source', 'information.header.map') . " \n";
+// Encode the array to JSON before inserting it into the Javascript block
+$jsonDelimiterMap = json_encode($phpDelimiterMap);
- $defaultNames = [
- 'env_identifier_sourcekey' => 'ENV_SOURCE_KEY',
- 'env_address_street' => 'ENV_STREET',
- 'env_address_locality' => 'ENV_LOCALITY',
- 'env_address_state' => 'ENV_STATE',
- 'env_address_postalcode' => 'ENV_POSTALCODE',
- 'env_address_country' => 'ENV_COUNTRY',
- 'env_affiliation' => 'ENV_AFFILIATION',
- 'env_department' => 'ENV_DEPARTMENT',
- 'env_identifier_eppn' => 'ENV_EPPN',
- 'env_identifier_eptid' => 'ENV_EPTID',
- 'env_identifier_epuid' => 'ENV_EPUID',
- 'env_identifier_network' => 'ENV_NETWORK',
- 'env_identifier_oidcsub' => 'ENV_OIDCSUB',
- 'env_identifier_samlpairwiseid' => 'ENV_SAMLPAIRWISE',
- 'env_identifier_samlsubjectid' => 'ENV_SAMLSUBJECT',
- 'env_mail' => 'ENV_MAIL',
- 'env_name_honorific' => 'ENV_HONORIFIC',
- 'env_name_given' => 'ENV_GIVEN',
- 'env_name_middle' => 'ENV_MIDDLE',
- 'env_name_family' => 'ENV_FAMILY',
- 'env_name_suffix' => 'ENV_SUFFIX',
- 'env_organization' => 'ENV_ORGANIZATION',
- 'env_telephone_number' => 'ENV_TELEPHONE',
- 'env_title' => 'ENV_TITLE'
- ];
- foreach([
- 'env_identifier_sourcekey',
- 'env_address_street',
- 'env_address_locality',
- 'env_address_state',
- 'env_address_postalcode',
- 'env_address_country',
- 'env_affiliation',
- 'env_department',
- 'env_identifier_eppn',
- 'env_identifier_eptid',
- 'env_identifier_epuid',
- 'env_identifier_network',
- 'env_identifier_oidcsub',
- 'env_identifier_samlpairwiseid',
- 'env_identifier_samlsubjectid',
- 'env_mail',
- 'env_name_honorific',
- 'env_name_given',
- 'env_name_middle',
- 'env_name_family',
- 'env_name_suffix',
- 'env_organization',
- 'env_telephone_number',
- 'env_title'
- ] as $field) {
- print $this->element('form/listItem', [
- 'arguments' => [
- 'fieldName' => $field,
- 'fieldOptions' => [
- 'default' => $defaultNames[$field]
- ]
- ]
- ]);
- }
+$afterField = <<
+document.addEventListener('DOMContentLoaded', function() {
+ var select = document.getElementById('mva_delimiter_default');
+ var textbox = document.getElementById('mva_delimiter');
+
+ // Map the dropdown values to actual delimiter characters
+ var delimiterMap = $jsonDelimiterMap;
+
+ select.addEventListener('change', function() {
+ var selectedValue = select.value;
+
+ // Update the textbox with the corresponding delimiter
+ if (delimiterMap[selectedValue] !== undefined) {
+ textbox.value = delimiterMap[selectedValue];
+ } else {
+ // Fallback in case of unexpected values
+ textbox.value = selectedValue;
+ }
+
+ // Focus the textbox if "Other" is selected so we can immediately type
+ if (selectedValue === 'O') {
+ textbox.focus();
+ }
+ });
+
+ if (!textbox.value && select.value) {
+ textbox.value = delimiterMap[select.value] || '';
+ }
+});
+
+JS;
+
+
+$fields = [
+ // 'duplicate_mode',
+ 'mva_delimiter' => [
+ 'fieldLabel' => __d('env_source','field.EnvSources.mva_delimiter'),
+ 'groupedControls' => [
+ 'mva_delimiter' => [
+ 'type' => 'text',
+ 'id' => 'mva_delimiter',
+ 'label' => false,
+ 'required' => true,
+ 'class' => 'form-control mb-1',
+ 'singleRowItem' => true,
+ ],
+ 'mva_delimiter_default' => [
+ 'id' => 'mva_delimiter_default',
+ 'label' => [
+ 'text' => __d('env_source','field.EnvSources.mva_delimiter_common'),
+ 'class' => 'mb-2'
+ ],
+ 'name' => 'mva_delimiter_default',
+ 'required' => false,
+ 'empty' => false,
+ 'type' => 'select',
+ 'value' => $selectedValue,
+ 'options' => $spModes,
+ ],
+ ],
+ 'afterField' => $afterField
+ ],
+ 'sync_on_login',
+ 'address_type_id',
+ 'default_affiliation_type_id',
+ 'email_address_type_id',
+ 'name_type_id',
+ 'telephone_number_type_id',
+ 'redirect_on_duplicate',
+ 'lookaside_file',
+ 'SUBTITLE' => [
+ 'subtitle' => __d('env_source', 'information.header.map')
+ ]
+];
+
+$defaultNames = [
+ 'env_identifier_sourcekey' => 'ENV_SOURCE_KEY',
+ 'env_address_street' => 'ENV_STREET',
+ 'env_address_locality' => 'ENV_LOCALITY',
+ 'env_address_state' => 'ENV_STATE',
+ 'env_address_postalcode' => 'ENV_POSTALCODE',
+ 'env_address_country' => 'ENV_COUNTRY',
+ 'env_affiliation' => 'ENV_AFFILIATION',
+ 'env_department' => 'ENV_DEPARTMENT',
+ 'env_identifier_eppn' => 'ENV_EPPN',
+ 'env_identifier_eptid' => 'ENV_EPTID',
+ 'env_identifier_epuid' => 'ENV_EPUID',
+ 'env_identifier_network' => 'ENV_NETWORK',
+ 'env_identifier_oidcsub' => 'ENV_OIDCSUB',
+ 'env_identifier_samlpairwiseid' => 'ENV_SAMLPAIRWISE',
+ 'env_identifier_samlsubjectid' => 'ENV_SAMLSUBJECT',
+ 'env_mail' => 'ENV_MAIL',
+ 'env_name_honorific' => 'ENV_HONORIFIC',
+ 'env_name_given' => 'ENV_GIVEN',
+ 'env_name_middle' => 'ENV_MIDDLE',
+ 'env_name_family' => 'ENV_FAMILY',
+ 'env_name_suffix' => 'ENV_SUFFIX',
+ 'env_organization' => 'ENV_ORGANIZATION',
+ 'env_telephone_number' => 'ENV_TELEPHONE',
+ 'env_title' => 'ENV_TITLE'
+];
+
+foreach($defaultNames as $field => $envName) {
+ $fields[$field] = [
+ 'default' => $envName
+ ];
}
+
+$subnav = [
+ 'tabs' => ['ExternalIdentitySources', 'EnvSource.EnvSources', 'ExternalIdentitySources@action.search'],
+ 'action' => [
+ 'ExternalIdentitySources' => ['edit', 'view', 'search'],
+ 'EnvSource.EnvSources' => ['edit'],
+ 'ExternalIdentitySources@action.search' => [],
+ ],
+];
diff --git a/app/plugins/OrcidSource/.gitignore b/app/plugins/OrcidSource/.gitignore
new file mode 100644
index 000000000..244d127b1
--- /dev/null
+++ b/app/plugins/OrcidSource/.gitignore
@@ -0,0 +1,8 @@
+/composer.lock
+/composer.phar
+/phpunit.xml
+/.phpunit.result.cache
+/phpunit.phar
+/config/Migrations/schema-dump-default.lock
+/vendor/
+/.idea/
diff --git a/app/plugins/OrcidSource/README.md b/app/plugins/OrcidSource/README.md
new file mode 100644
index 000000000..14821f1da
--- /dev/null
+++ b/app/plugins/OrcidSource/README.md
@@ -0,0 +1,11 @@
+# OrcidSource plugin for CakePHP
+
+## Installation
+
+You can install this plugin into your CakePHP application using [composer](https://getcomposer.org).
+
+The recommended way to install composer packages is:
+
+```
+composer require your-name-here/orcid-source
+```
diff --git a/app/plugins/OrcidSource/composer.json b/app/plugins/OrcidSource/composer.json
new file mode 100644
index 000000000..83e4a9222
--- /dev/null
+++ b/app/plugins/OrcidSource/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "your-name-here/orcid-source",
+ "description": "OrcidSource plugin for CakePHP",
+ "type": "cakephp-plugin",
+ "license": "MIT",
+ "require": {
+ "php": ">=7.2",
+ "cakephp/cakephp": "4.6.*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "OrcidSource\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "OrcidSource\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ }
+ }
+}
diff --git a/app/plugins/OrcidSource/config/plugin.json b/app/plugins/OrcidSource/config/plugin.json
new file mode 100644
index 000000000..e3113e8b1
--- /dev/null
+++ b/app/plugins/OrcidSource/config/plugin.json
@@ -0,0 +1,73 @@
+{
+ "types": {
+ "enrollment_flow_step": [
+ "OrcidSourceCollectors"
+ ],
+ "external_identity_source": [
+ "OrcidSources"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "orcid_source_collectors": {
+ "columns": {
+ "id": {},
+ "enrollment_flow_step_id": {},
+ "external_identity_source_id": {}
+ },
+ "indexes": {
+ "orcid_source_collectors_i1": { "columns": [ "enrollment_flow_step_id" ] },
+ "orcid_source_collectors_i2": { "needed": false, "columns": [ "external_identity_source_id" ] }
+ }
+ },
+ "orcid_sources": {
+ "columns": {
+ "id": {},
+ "external_identity_source_id": {},
+ "server_id": { "type": "integer", "foreignkey": { "table": "servers", "column": "id" }, "notnull": false },
+ "scope_inherit": { "type": "boolean" },
+ "api_tier": { "type": "string", "size": "3" },
+ "api_type": { "type": "string", "size": "3" },
+ "default_affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "email_address_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "name_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } },
+ "telephone_number_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }
+ },
+ "indexes": {
+ "orcid_sources_i1": { "columns": [ "external_identity_source_id" ] }
+ }
+ },
+ "orcid_tokens": {
+ "columns": {
+ "id": {},
+ "orcid_source_id": { "type": "integer", "foreignkey": { "table": "orcid_sources", "column": "id" }, "notnull": true },
+ "orcid_identifier": { "type": "string", "size": "128" },
+ "access_token": { "type": "text" },
+ "id_token": { "type": "text" },
+ "refresh_token": { "type": "text" }
+ },
+ "indexes": {
+ "orcid_source_tokens_i1": {
+ "columns": [ "orcid_source_id", "orcid_identifier"]
+ },
+ "orcid_source_tokens_i2": { "columns": [ "orcid_identifier" ] },
+ "orcid_source_tokens_i3": { "needed": false, "columns": [ "orcid_source_id" ] }
+ }
+ },
+ "petition_orcids": {
+ "columns": {
+ "id": {},
+ "petition_id": {},
+ "orcid_source_collector_id": { "type": "integer", "foreignkey": { "table": "orcid_source_collectors", "column": "id" }, "notnull": true },
+ "orcid_identifier": { "type": "string", "size": "128" },
+ "orcid_token": { "type": "text" }
+ },
+ "indexes": {
+ "petition_orcids_i1": { "columns": [ "petition_id" ] },
+ "petition_orcids_i2": { "columns": [ "orcid_source_collector_id" ] }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/OrcidSource/config/routes.php b/app/plugins/OrcidSource/config/routes.php
new file mode 100644
index 000000000..0fe23ea2c
--- /dev/null
+++ b/app/plugins/OrcidSource/config/routes.php
@@ -0,0 +1,58 @@
+scope('/api/orcidsource', function (RouteBuilder $builder) {
+ // Register scoped middleware for in scopes.
+// Do not enable CSRF for the REST API, it will break standard (non-AJAX) clients
+// $builder->registerMiddleware('csrf', new CsrfProtectionMiddleware(['httponly' => true]));
+ // BodyParserMiddleware will automatically parse JSON bodies, but we only
+ // want that for API transactions, so we only apply it to the /api scope.
+ $builder->registerMiddleware('bodyparser', new BodyParserMiddleware());
+ /*
+ * Apply a middleware to the current route scope.
+ * Requires middleware to be registered through `Application::routes()` with `registerMiddleware()`
+ */
+// Do not enable CSRF for the REST API, it will break standard (non-AJAX) clients
+// $builder->applyMiddleware('csrf');
+ $builder->setExtensions(['json']);
+ $builder->applyMiddleware('bodyparser');
+ $builder->get(
+ '/v2/token/{orcid}/co/{coId}',
+ ['plugin' => 'OrcidSource', 'controller' => 'ApiV2', 'action' => 'get']
+ )
+ ->setPass(['orcid', 'coId'])
+ ->setPatterns([
+ 'orcid' => '([0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4})',
+ 'coId' => '[0-9]+',
+ ]);
+});
\ No newline at end of file
diff --git a/app/plugins/OrcidSource/phpunit.xml.dist b/app/plugins/OrcidSource/phpunit.xml.dist
new file mode 100644
index 000000000..f828b8533
--- /dev/null
+++ b/app/plugins/OrcidSource/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+
+ src/
+
+
+
diff --git a/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po b/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po
new file mode 100644
index 000000000..decc5298a
--- /dev/null
+++ b/app/plugins/OrcidSource/resources/locales/en_US/orcid_source.po
@@ -0,0 +1,113 @@
+# COmanage Registry Localizations (orcid_source domain)
+#
+# Portions licensed to the University Corporation for Advanced Internet
+# Development, Inc. ("UCAID") under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information
+# regarding copyright ownership.
+#
+# UCAID licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# @link https://www.internet2.edu/comanage COmanage Project
+# @package registry
+# @since COmanage Registry v5.2.0
+# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+msgid "controller.OrcidSourceCollectors"
+msgstr "{0,plural,=1{Orcid Source Collector} other{Orcid Source Collectors}}"
+
+msgid "controller.OrcidSources"
+msgstr "{0,plural,=1{Orcid Source} other{Orcid Sources}}"
+
+msgid "controller.PetitionOrcidIdentities"
+msgstr "{0,plural,=1{Petition Orcid Identity} other{Petition Orcid Identities}}"
+
+msgid "enumeration.OrcidSourceTierEnum.PRO"
+msgstr "Production"
+
+msgid "enumeration.OrcidSourceTierEnum.SBX"
+msgstr "Sandbox"
+
+msgid "enumeration.OrcidSourceApiEnum.AUT"
+msgstr "Authorize"
+
+msgid "enumeration.OrcidSourceApiEnum.MEM"
+msgstr "Members"
+
+msgid "enumeration.OrcidSourceApiEnum.PUB"
+msgstr "Public"
+
+msgid "error.search"
+msgstr "Search request returned {0}"
+
+msgid "error.token.none"
+msgstr "Access token not configured (try resaving configuration)"
+
+msgid "error.param.notfound"
+msgstr "{0} was not found"
+
+msgid "error.response.no_orcid"
+msgstr "ORCID identifier missing from response."
+
+msgid "error.exists"
+msgstr "Orcid Token already exists with this Identifier"
+
+msgid "field.OrcidSources.api_type"
+msgstr "API Type"
+
+msgid "field.OrcidSources.redirect_uri"
+msgstr "Additional ORCID Redirect URI"
+
+msgid "field.OrcidSources.scope_inherit"
+msgstr "Inherit Scope"
+
+msgid "field.OrcidSources.api_tier"
+msgstr "API Tier"
+
+msgid "field.OrcidSources.name_type_id"
+msgstr "Name Type"
+
+msgid "field.OrcidSources.telephone_number_type_id"
+msgstr "Telephone Number Type"
+
+msgid "field.OrcidSources.address_type_id"
+msgstr "Address Type"
+
+msgid "field.OrcidSources.default_affiliation_type_id"
+msgstr "Default Affiliation Type"
+
+msgid "field.OrcidSources.email_address_type_id"
+msgstr "Email Address Type"
+
+msgid "information.OrcidSources.linked"
+msgstr "Obtained ORCID {0} via authenticated OAuth flow"
+
+msgid "information.orcid_source.identifier"
+msgstr "ORCID Identifier"
+
+msgid "information.OrcidSourceCollectors.authenticate"
+msgstr "Authenticate with ORCID"
+
+msgid "information.OrcidSourceCollectors.sign_in"
+msgstr "Sign in with your ORCID account to securely verify your ORCID iD."
+
+msgid "information.OrcidSources.default.types"
+msgstr "Select Default Types for ORCID Fields"
+
+msgid "result.OrcidSourceCollector.collected"
+msgstr "Obtained ORCID Identifier {0}"
+
+msgid "result.orcid.saved"
+msgstr "ORCID Token recorded"
+
+msgid "result.pipeline.status"
+msgstr "OrcidSource Pipeline completed with status {0}"
diff --git a/app/plugins/OrcidSource/src/Controller/ApiV2Controller.php b/app/plugins/OrcidSource/src/Controller/ApiV2Controller.php
new file mode 100644
index 000000000..22ff3307c
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Controller/ApiV2Controller.php
@@ -0,0 +1,207 @@
+request->getParam('coId') ?? null;
+ }
+
+
+ public function initialize(): void {
+ parent::initialize();
+ $this->OrcidSources = TableRegistry::getTableLocator()->get('OrcidSource.OrcidSources');
+ $this->OrcidTokens = TableRegistry::getTableLocator()->get('OrcidSource.OrcidTokens');
+ }
+
+ /**
+ * Calculate authorization for the current request.
+ *
+ * @since COmanage Registry v5.2.0
+ * @return bool True if the current request is permitted, false otherwise
+ */
+
+ public function calculatePermission(): bool {
+ $authUser = $this->RegistryAuth->getAuthenticatedUser();
+ $coid = $this->calculateRequestedCOID();
+
+ return $authUser !== null
+ && $this->RegistryAuth->isApiUser()
+ && ($this->RegistryAuth->isPlatformAdmin() || $this->RegistryAuth->isCoAdmin($coid));
+ }
+
+ /**
+ * Handle a Get SOR Person Role request.
+ *
+ * @param string $orcid
+ * @param int $coId
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function get(string $orcid, int $coId) {
+ try {
+ $orcidSourcesRecords = $this->OrcidSources
+ ->find()
+ ->contain([
+ 'Servers',
+ 'ExternalIdentitySources',
+ ])
+ ->innerJoinWith('Servers')
+ ->innerJoinWith('ExternalIdentitySources')
+ ->where([
+ 'Servers.plugin' => 'CoreServer.Oauth2Servers',
+ 'ExternalIdentitySources.co_id' => $coId,
+ 'ExternalIdentitySources.plugin' => 'OrcidSource.OrcidSources'
+ ])
+ ->disableHydration()
+ ->all()
+ ->toArray();
+
+ // Extract OrcidSource IDs
+ $orcid_source_ids = Hash::extract($orcidSourcesRecords, '{n}.id');
+
+ // Find token records from the database
+ $tokens = $this->OrcidTokens->find()
+ ->where([
+ 'OrcidTokens.orcid_identifier' => $orcid,
+ 'OrcidTokens.orcid_source_id IN' => $orcid_source_ids
+ ])
+ ->disableHydration()
+ ->all()
+ ->toArray();
+
+ $columnsToDecrypt = [
+ 'access_token',
+ 'id_token',
+ 'refresh_token'
+ ];
+
+ if (count($tokens) === 0) {
+ throw new RecordNotFoundException(__d('orcid_source', 'error.param.notfound', [__d('orcid_source', 'information.orcid_source.identifier')]));
+ }
+
+ foreach ($tokens as $idx => $token) {
+ $orcidSourceIndex = array_search($token['orcid_source_id'], $orcid_source_ids);
+ $tokens[$idx]['scopes'] = $this->getOauth2ServerScopes(
+ $orcidSourcesRecords[$orcidSourceIndex]['server'],
+ $orcidSourcesRecords[$orcidSourceIndex]
+ );
+ foreach ($columnsToDecrypt as $column) {
+ $value = $token[$column] ?? null;
+ $tokens[$idx][$column] = !empty($value) ? $this->OrcidTokens->getUnencrypted($value) : '';
+ }
+ }
+
+ // Return data in structured format
+ $this->set('orcid_tokens', $tokens);
+ $this->set('vv_model_name', 'OrcidTokens');
+ $this->set('vv_table_name', 'orcid_tokens');
+ }
+ catch(RecordNotFoundException $e) {
+ // Return 404
+ $this->response = $this->response->withStatus(
+ HttpStatusCodesEnum::HTTP_NOT_FOUND,
+ $e->getMessage()
+ );
+ $this->autoRender = false;
+ return;
+ }
+ catch(\Exception $e) {
+ // Return 400
+ $this->response = $this->response->withStatus(
+ HttpStatusCodesEnum::HTTP_BAD_REQUEST,
+ $e->getMessage()
+ );
+ $this->autoRender = false;
+ return;
+ }
+
+ // Let the view render
+ $this->render('/Standard/api/v2/json/index');
+ }
+
+ /**
+ * Indicate whether this Controller will handle some or all authnz.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param EventInterface $event Cake event, ie: from beforeFilter
+ * @return string "no", "open", "authz", or "yes"
+ */
+
+ public function willHandleAuth(\Cake\Event\EventInterface $event): string {
+ // We always take over authz
+ return 'authz';
+ }
+
+ /**
+ * Get the scopes
+ *
+ * @param array $server Server Record
+ * @param array $orcidSource OrcidSource record
+ *
+ * @return string List of scopes
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function getOauth2ServerScopes(array $server, array $orcidSource): string
+ {
+ if(is_bool($orcidSource['scope_inherit']) && $orcidSource['scope_inherit']) {
+ $Oauth2ServersTable = TableRegistry::getTableLocator()->get('Oauth2Servers');
+ $oauth2Server = $Oauth2ServersTable->find()
+ ->select(['scope'])
+ ->where(['server_id' => $server['id']])
+ ->first();
+
+ if ($oauth2Server && !empty($oauth2Server->scope)) {
+ return $oauth2Server->scope;
+ }
+ }
+
+ return OrcidSourceScopeEnum::DEFAULT_SCOPE;
+ }
+}
diff --git a/app/plugins/OrcidSource/src/Controller/AppController.php b/app/plugins/OrcidSource/src/Controller/AppController.php
new file mode 100644
index 000000000..9e22929f7
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+ [
+ 'OrcidSourceCollectors.id' => 'asc'
+ ]
+ ];
+
+ /**
+ * Callback run prior to the request action.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param EventInterface $event Cake Event
+ */
+ public function beforeFilter(\Cake\Event\EventInterface $event)
+ {
+ $this->OrcidSources = TableRegistry::getTableLocator()->get('OrcidSource.OrcidSources');
+ $this->PetitionOrcids = TableRegistry::getTableLocator()->get('OrcidSource.PetitionOrcids');
+
+ parent::beforeFilter($event);
+ }
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @return Response|void
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->OrcidSourceCollectors->EnrollmentFlowSteps->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->OrcidSourceCollectors->EnrollmentFlowSteps->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->OrcidSourceCollectors->EnrollmentFlowSteps->getPrimaryKey());
+ }
+
+ return parent::beforeRender($event);
+ }
+
+ /**
+ * Dispatch an Enrollment Flow Step.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param string $id Env Source Collector ID
+ */
+
+ public function dispatch(string $id) {
+ $request = $this->getRequest();
+ $session = $request->getSession();
+
+ $op = $this->requestParam('op');
+ $code = $this->getRequest()->getQuery('code') ?? null;
+ $petition = $this->getPetition();
+
+ $this->set('vv_op', $op);
+
+ $oricdSourceEntity = $this->OrcidSourceCollectors->get(
+ (int)$id,
+ [
+ 'contain' => [
+ 'ExternalIdentitySources' => ['OrcidSources' => ['Servers']]
+ ]]
+ );
+
+ $ServerModel = $oricdSourceEntity->external_identity_source->orcid_source->server->plugin;
+ $PluginServersTable = TableRegistry::getTableLocator()->get($ServerModel);
+ $serverId = $oricdSourceEntity->external_identity_source->orcid_source->server->id;
+ $PluginServerEntity = $PluginServersTable ->find()
+ ->where(['server_id' => $serverId])
+ ->first();
+
+
+ $this->set('vv_config', $oricdSourceEntity);
+ $this->set('vv_config_server', $PluginServerEntity);
+ $this->set('controller', $this);
+
+ try {
+ // Let's authenticate first
+ if ($op == 'authenticate') {
+ $this->authenticate($id, $PluginServerEntity);
+ } else if (!empty($code) && $op !== 'savetoken') {
+ $response = $PluginServersTable->exchangeCode(
+ $id,
+ $code,
+ $this->OrcidSources->redirectUri(
+ [
+ $id,
+ '?' => ['petition_id' => $petition->id],
+ ]
+ ),
+ false
+ );
+
+ // Use the response and save the data to petitions table
+ if(empty($response->orcid)) {
+ throw new \RuntimeException(__d('orcid_source', 'error.response.no_orcid'));
+ }
+ $this->set('vv_orcid', $response->orcid);
+ $this->set('vv_token', $response);
+ } if (!empty($code) && $op === 'savetoken') {
+ $orcid_token = $this->requestParam('orcid_token');
+ $this->PetitionOrcids->record(
+ petitionId: $petition->id,
+ enrollmentFlowStepId: $oricdSourceEntity->enrollment_flow_step_id,
+ orcidToken: $orcid_token,
+ orcidSourceCollectorId: (int)$id,
+ );
+ // On success, indicate the step is completed and generate a redirect
+ // to the next step
+
+ return $this->finishStep(
+ enrollmentFlowStepId: $oricdSourceEntity->enrollment_flow_step_id,
+ petitionId: $petition->id,
+ comment: __d('orcid_source', 'result.orcid.saved')
+ );
+ } else {
+ // Fall Through. Let the view render
+ }
+
+ }
+ catch(\Exception $e) {
+ $this->Flash->error($e->getMessage());
+ }
+
+ // Fall through and let the form render
+
+ $this->render('/Standard/dispatch');
+ }
+
+
+ /**
+ * Authenticate the user with ORCID OAuth2 server
+ *
+ * @param string|int $id ID of the collector
+ * @param EntityInterface $serverCfg ORCID Server configuration
+ * @return void
+ * @since COmanage Registry v5.2.0
+ */
+ protected function authenticate(string|int $id, EntityInterface $serverCfg): void
+ {
+ $petition = $this->getPetition();
+ $callback = $this->OrcidSources->redirectUri([
+ $id,
+ '?' => ['petition_id' => $petition->id],
+ ]);
+ // Build the redirect URI
+ $redirectUri = Router::url($callback, true);
+
+ $scope = OrcidSourceScopeEnum::DEFAULT_SCOPE;
+ if (!empty($serverCfg->scope_inherit)) {
+ $scope = $serverCfg->scope_inherit;
+ }
+
+ $url = $serverCfg->url . '/authorize?';
+ $url .= 'client_id=' . $serverCfg->clientid;
+ $url .= '&response_type=code';
+ $url .= '&scope=' . str_replace(' ', '%20', $scope);
+ $url .= '&redirect_uri=' . urlencode($redirectUri);
+
+ $this->redirect($url);
+ }
+
+ /**
+ * Indicate whether this Controller will handle some or all authnz.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param EventInterface $event Cake event, ie: from beforeFilter
+ * @return string "no", "open", "authz", or "yes"
+ */
+
+ public function willHandleAuth(\Cake\Event\EventInterface $event): string {
+ $request = $this->getRequest();
+ $action = $request->getParam('action');
+
+ if($action == 'dispatch') {
+ // We need to perform special logic (vs StandardEnrollerController)
+ // to ensure that web server authentication is triggered.
+ // (This is the same logic as IdentifierCollectorsController.)
+// XXX We could maybe move this into StandardEnrollerController with a flag like
+// $this->alwaysAuthDispatch(true);
+
+ // To start, we trigger the parent logic. This will return
+ // notauth: Some error occurred, we don't want to override this
+ // authz: No token in use
+ // yes: Token validated
+
+ $auth = parent::willHandleAuth($event);
+
+ // The only status we need to override is 'yes', since we always want authentication
+ // to run in order to be able to grab $REMOTE_USER.
+
+ return ($auth == 'yes' ? 'authz' : $auth);
+ }
+
+ return parent::willHandleAuth($event);
+ }
+}
diff --git a/app/plugins/OrcidSource/src/Controller/OrcidSourcesController.php b/app/plugins/OrcidSource/src/Controller/OrcidSourcesController.php
new file mode 100644
index 000000000..61a7cabcc
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Controller/OrcidSourcesController.php
@@ -0,0 +1,64 @@
+ [
+ 'EnvSources.id' => 'asc'
+ ]
+ ];
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @return Response|void
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->OrcidSources->ExternalIdentitySources->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->OrcidSources->ExternalIdentitySources->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->OrcidSources->ExternalIdentitySources->getPrimaryKey());
+ }
+ $this->set('vv_redirect_uri', $this->OrcidSources->redirectUri());
+
+ return parent::beforeRender($event);
+ }
+}
diff --git a/app/plugins/OrcidSource/src/Controller/OrcidTokensController.php b/app/plugins/OrcidSource/src/Controller/OrcidTokensController.php
new file mode 100644
index 000000000..ab60d37f1
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Controller/OrcidTokensController.php
@@ -0,0 +1,36 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/OrcidSource/src/Model/Entity/OrcidSourceCollector.php b/app/plugins/OrcidSource/src/Model/Entity/OrcidSourceCollector.php
new file mode 100644
index 000000000..dfc309d06
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Entity/OrcidSourceCollector.php
@@ -0,0 +1,51 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/OrcidSource/src/Model/Entity/OrcidToken.php b/app/plugins/OrcidSource/src/Model/Entity/OrcidToken.php
new file mode 100644
index 000000000..5ca584d67
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Entity/OrcidToken.php
@@ -0,0 +1,51 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/OrcidSource/src/Model/Entity/PetitionOrcid.php b/app/plugins/OrcidSource/src/Model/Entity/PetitionOrcid.php
new file mode 100644
index 000000000..fdca95859
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Entity/PetitionOrcid.php
@@ -0,0 +1,51 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php b/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php
new file mode 100644
index 000000000..809a89471
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Table/OrcidSourceCollectorsTable.php
@@ -0,0 +1,199 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+
+ $this->belongsTo('EnrollmentFlowSteps');
+ $this->belongsTo('ExternalIdentitySources');
+
+ $this->setDisplayField('id');
+
+ $this->setPrimaryLink('enrollment_flow_step_id');
+ $this->setRequiresCO(true);
+ $this->setAllowLookupPrimaryLink(['dispatch', 'display']);
+
+ $this->setAutoViewVars([
+ 'externalIdentitySources' => [
+ 'type' => 'plugin',
+ 'model' => 'OrcidSource.OrcidSources',
+ ]
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false, // Delete the pluggable object instead
+ 'dispatch' => true,
+ 'display' => true,
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false, // This is added by the parent model
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Perform steps necessary to hydrate the Person record as part of Petition finalization.
+ *
+ * @param int $id Env Source Collector ID
+ * @param Petition $petition Petition
+ * @return bool true on success
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function hydrate(int $id, \App\Model\Entity\Petition $petition) {
+ $orcidSourceCollectorsEntity = $this->get($id);
+
+ $PetitionHistoryRecords = TableRegistry::getTableLocator()->get('PetitionHistoryRecords');
+ $PetitionOrcids = TableRegistry::getTableLocator()->get('OrcidSource.PetitionOrcids');
+ $OrcidSources = TableRegistry::getTableLocator()->get('OrcidSource.OrcidSources');
+ $OrcidTokens = TableRegistry::getTableLocator()->get('OrcidSource.OrcidTokens');
+ $ExtIdentitySources = TableRegistry::getTableLocator()->get('ExternalIdentitySources');
+
+
+ $orcid_source = $OrcidSources->find()
+ ->where(['external_identity_source_id' => $orcidSourceCollectorsEntity->external_identity_source_id])
+ ->first();
+
+ $pOricd = $PetitionOrcids
+ ->find()
+ ->where(
+ ['petition_id' => $petition->id, 'orcid_source_collector_id' => $id])
+ ->first();
+
+ if(!empty($pOricd->orcid_token)) {
+ // Copy the Identifier to the Person record in accordance with the configuration
+ $token = unserialize($pOricd->orcid_token);
+ $data = [
+ 'orcid_identifier' => $token->orcid,
+ 'access_token' => $token->access_token,
+ 'refresh_token' => $token->refresh_token ?? '',
+ 'id_token' => $token->id_token ?? '',
+ 'orcid_source_id' => $orcid_source->id
+ ];
+
+ $OrcidTokens->upsertOrFail(
+ data: $data,
+ whereClause: [
+ 'orcid_source_id' => $orcid_source->id,
+ 'orcid_identifier' => $token->orcid
+ ],
+ );
+
+ // Continue on to process the sync
+ // Trigger the ExternalIdentitySource sync and push the data to the pipeline
+ $status = $ExtIdentitySources->sync(
+ id: $orcidSourceCollectorsEntity->external_identity_source_id,
+ sourceKey: $token->orcid,
+ personId: $petition->enrollee_person_id,
+ syncOnly: true
+ );
+
+ // Record Petition History - Token Save
+ $PetitionHistoryRecords->record(
+ petitionId: $petition->id,
+ enrollmentFlowStepId: $orcidSourceCollectorsEntity->enrollment_flow_step_id,
+ action: PetitionActionEnum::AttributesUpdated,
+ comment: __d('orcid_source', 'result.orcid.saved')
+ );
+
+ // Record Petition History - Pipeline save
+ $PetitionHistoryRecords->record(
+ petitionId: $petition->id,
+ enrollmentFlowStepId: $orcidSourceCollectorsEntity->enrollment_flow_step_id,
+ action: PetitionActionEnum::Finalized,
+ comment: __d('orcid_source', 'result.pipeline.status', [$status])
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('enrollment_flow_step_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('enrollment_flow_step_id');
+
+ $validator->add('external_identity_source_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('external_identity_source_id');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php b/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php
new file mode 100644
index 000000000..60745cddd
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Table/OrcidSourcesTable.php
@@ -0,0 +1,587 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('ExternalIdentitySources');
+ $this->belongsTo('Servers');
+ $this->belongsTo('AddressTypes')
+ ->setClassName('Types')
+ ->setForeignKey('address_type_id')
+ ->setProperty('address_type');
+ $this->belongsTo('DefaultAffiliationTypes')
+ ->setClassName('Types')
+ ->setForeignKey('default_affiliation_type_id')
+ ->setProperty('default_affiliation_type');
+ $this->belongsTo('EmailAddressTypes')
+ ->setClassName('Types')
+ ->setForeignKey('email_address_type_id')
+ ->setProperty('email_address_type');
+ $this->belongsTo('NameTypes')
+ ->setClassName('Types')
+ ->setForeignKey('name_type_id')
+ ->setProperty('name_type');
+ $this->belongsTo('TelephoneNumberTypes')
+ ->setClassName('Types')
+ ->setForeignKey('telephone_number_type_id')
+ ->setProperty('telephone_number_type');
+
+ $this->hasMany('OrcidSource.OrcidTokens')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+
+ $this->setDisplayField('id');
+
+ $this->setPrimaryLink(['external_identity_source_id']);
+ $this->setRequiresCO(true);
+
+ $this->setEditContains([
+ 'Servers' => ['Oauth2Servers'],
+ 'ExternalIdentitySources',
+ ]);
+
+ $this->setViewContains([
+ 'Servers' => ['Oauth2Servers'],
+ 'ExternalIdentitySources',
+ ]);
+
+ $this->setAutoViewVars([
+ 'servers' => [
+ 'type' => 'plugin',
+ 'model' => 'CoreServer.Oauth2Servers'
+ ],
+ 'api_tiers' => [
+ 'type' => 'enum',
+ 'class' => 'OrcidSource.OrcidSourceTierEnum'
+ ],
+ 'api_types' => [
+ 'type' => 'enum',
+ 'class' => 'OrcidSource.OrcidSourceApiEnum'
+ ],
+ 'addressTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'Addresses.type'
+ ],
+ 'defaultAffiliationTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'PersonRoles.affiliation_type'
+ ],
+ 'emailAddressTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'EmailAddresses.type'
+ ],
+ 'nameTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'Names.type'
+ ],
+ 'telephoneNumberTypes' => [
+ 'type' => 'type',
+ 'attribute' => 'TelephoneNumbers.type'
+ ]
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false, // Delete the pluggable object instead
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false, //['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+
+ $this->orcidTokensTable = TableRegistry::getTableLocator()->get('OrcidSource.OrcidTokens');
+ $this->oauth2ServersTable = TableRegistry::getTableLocator()->get('CoreServer.Oauth2Servers');
+ }
+
+
+ /**
+ * Get the OAuth2 redirect URI for ORCID callbacks
+ *
+ * @param array $extra Additional URL parameters to include in redirect
+ * @return string Full URL for OAuth2 redirect
+ * @since COmanage Registry v5.2.0
+ */
+ public function redirectUri(array $extra = []): string
+ {
+ $callback = [
+ 'plugin' => 'OrcidSource',
+ 'controller' => 'OrcidSourceCollectors',
+ 'action' => 'dispatch',
+ ];
+
+ if (!empty($extra)) {
+ $callback = array_merge($callback, $extra);
+ }
+
+ return Router::url($callback, true);
+ }
+
+ /**
+ * Obtain the set of changed records from the source database.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param ExternalIdentitySource $source External Identity Source
+ * @param int $lastStart Timestamp of last run
+ * @param int $curStart Timestamp of current run
+ * @return array|bool An array of changed source keys, or false
+ */
+
+ public function getChangeList(
+ \App\Model\Entity\ExternalIdentitySource $source,
+ int $lastStart, // timestamp of last run
+ int $curStart // timestamp of current run
+ ): array|bool {
+ return false;
+ }
+
+ /**
+ * Obtain the full set of records from the source database.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param ExternalIdentitySource $source External Identity Source
+ * @return array An array of source keys
+ */
+
+ public function inventory(
+ \App\Model\Entity\ExternalIdentitySource $source
+ ): array {
+ return false;
+ }
+
+ /**
+ * Convert a record from the OrcidSource data to a record suitable for
+ * construction of an Entity. This call is for use with Relational Mode.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param OrcidSource $OrcidSource OrcidSource configuration entity
+ * @param array $result Array of Orcid attributes
+ * @return array Entity record (in array format)
+ */
+
+ protected function resultToEntityData(
+ OrcidSource $OrcidSource,
+ array $result
+ ): array {
+ // Build the External Identity as an array
+ $eidata = [];
+
+ // We don't currently have a field to record DoB, so we need to null it
+ $eidata['date_of_birth'] = null;
+
+ // Single value fields that map to the External Identity Role
+ $role = [
+ // We only support one role per record
+ 'role_key' => '1',
+ 'affiliation' => $this->DefaultAffiliationTypes->getTypeLabel($OrcidSource->default_affiliation_type_id)
+ ];
+
+ $eidata['external_identity_roles'][] = $role;
+
+ $name = [
+ 'type' => $this->NameTypes->getTypeLabel($OrcidSource->name_type_id),
+ 'given' => $result['name']['given-names']['value'],
+ 'family' => $result['name']['family-name']['value']
+ ];
+
+ $eidata['names'][] = $name;
+
+ foreach($result['emails']['email'] as $m) {
+ $eidata['email_addresses'][] = [
+ 'mail' => $m['email'],
+ 'type' => $this->EmailAddressTypes->getTypeLabel($OrcidSource->email_address_type_id),
+ 'verified' => $m['verified']
+ ];
+ }
+
+ if (!empty($result['addresses']['address'])) {
+ $address = [];
+ $address['type'] = $this->AddressTypes->getTypeLabel($OrcidSource->address_type_id);
+ foreach($result['addresses']['address'] as $ad) {
+ $address['country'] = $ad['country']['value'];
+ }
+ $eidata['addresses'][] = $address;
+ }
+
+ $eidata['identifiers'][] = [
+ 'identifier' => $result['name']['path'],
+ 'type' => 'orcid'
+ ];
+
+ return $eidata;
+ }
+
+ /**
+ * Retrieve a record from the External Identity Source.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param ExternalIdentitySource $source EIS Entity with instantiated plugin configuration
+ * @param string $source_key Backend source key for requested record
+ * @return array Array of source_key, source_record, and entity_data
+ * @throws InvalidArgumentException
+ */
+
+ public function retrieve(
+ \App\Model\Entity\ExternalIdentitySource $source,
+ string $source_key
+ ): array {
+ try {
+ $this->httpClient = $this->orcidConnect($source, $source_key);
+
+ $orcidbio = $this->orcidRequest('/v3.0/' . $source_key . '/person');
+// $orcidActivities = $this->orcidRequest('/v3.0/' . $source_key . '/activities');
+ }
+ catch(InvalidArgumentException $e) {
+ throw new \InvalidArgumentException(__d('error', 'unknown.identifier', [$source_key]));
+ }
+
+ return [
+ 'source_key' => $source_key,
+ 'source_record' => json_encode($orcidbio),
+ 'entity_data' => $this->resultToEntityData($source->orcid_source, $orcidbio)
+ ];
+ }
+
+ /**
+ * Search the External Identity Source.
+ * The ORCID search will be triggered by the CO/Platform Admin. As a result, we want to use a privileged access key
+ * i.e. the one the CO Admin got in Oauth2Server setup page
+ *
+ * refrence: https://info.orcid.org/documentation/api-tutorials/api-tutorial-searching-the-orcid-registry/
+ *
+ * @param ExternalIdentitySource $source EIS Entity with instantiated plugin configuration
+ * @param array $searchAttrs Array of search attributes and values, as configured by searchAttributes()
+ * @return array Array of matching records
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function search(
+ \App\Model\Entity\ExternalIdentitySource $source,
+ array $searchAttrs
+ ): array {
+ $ret = [];
+
+ if(!isset($searchAttrs['q'])) {
+ // For now, we only support free form search (though ORCID does support
+ // search by eg email).
+
+ return [];
+ }
+
+ // Turn the query string into an associative array
+ $queryString = $searchAttrs['q'];
+ if (!str_starts_with($searchAttrs['q'], 'q=')) {
+ $queryString = 'q=' . $searchAttrs['q'];
+ }
+ parse_str($queryString, $queryParts);
+ $searchAttrs = $queryParts;
+
+ // We just let search exceptions pop up the stack
+
+ $this->httpClient = $this->orcidConnect($source);
+
+ $records = $this->orcidRequest('/v3.0/search/', $searchAttrs);
+
+ if(isset($records['num-found']) && $records['num-found'] > 0) {
+ foreach($records['result'] as $rec) {
+ if(!empty($rec['orcid-identifier']['path'])) {
+ $orcid = $rec['orcid-identifier']['path'];
+
+ $orcidbio = $this->orcidRequest('/v3.0/' . $orcid . '/person');
+
+ if(!empty($orcidbio)) {
+ $ret[ $orcid ] = $this->resultToEntityData($source->orcid_source, $orcidbio);
+ }
+ }
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Get the set of searchable attributes for this backend.
+ *
+ * @since COmanage Registry v5.2.0
+ * @return array Array of searchable attributes and localized descriptions
+ */
+
+ public function searchableAttributes(): array {
+ return [
+ 'q' => __d('operation', 'search')
+ ];
+ }
+
+
+ /**
+ * Make an HTTP request to the ORCID API
+ *
+ * @param string $urlPath The API endpoint path to request
+ * @param array $data Request parameters or body data
+ * @param string $action HTTP method to use (get, post, etc)
+ * @return array Response data decoded from JSON
+ * @throws InvalidArgumentException If the ORCID identifier is invalid
+ * @throws RuntimeException If the API request fails
+ * @since COmanage Registry v5.2.0
+ */
+ public function orcidRequest(string $urlPath, array $data=[], string $action="get"): array
+ {
+ // Get the user access_token. If none is provided, then throw an exception
+ $accessToken = match(true) {
+ $this->orcidToken?->access_token !== null => $this->orcidTokensTable->getUnencrypted($this->orcidToken->access_token),
+ $this->orcidSource?->server?->oauth2_server?->access_token !== null => $this->orcidSource->server->oauth2_server->access_token,
+ default => throw new \InvalidArgumentException(__d('orcid_source', 'error.token.none'))
+ };
+
+ $options = [
+ 'headers' => [
+ 'Accept' => 'application/json',
+ 'Authorization' => 'Bearer ' . $accessToken,
+ 'Content-Type' => 'application/orcid+json'
+ ]
+ ];
+
+
+ // We do not need a token for public api and
+ if($this->orcidSource->api_type == OrcidSourceApiEnum::PUBLIC
+ && (
+ $urlPath == '/v3.0/search/'
+ || preg_match('#v3\.0/([0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4})/person#', $urlPath, $matches)
+ )) {
+ // No authentication is required for the public tier. Limited to 1000 requests per day.
+ unset($options['headers']['Authorization']);
+ }
+
+ $orcidUrlBase = $this->orcidUrl($this->orcidSource->api_type, $this->orcidSource->api_tier);
+ $fullUrl = $orcidUrlBase . $urlPath;
+ $response = $this->httpClient->$action(
+ url: $fullUrl,
+ data: ($action == 'get' ? $data : json_encode($data)),
+ options: $options
+ );
+
+ if($response->getStatusCode() == HttpStatusCodesEnum::HTTP_BAD_REQUEST) {
+ // Most likely retrieving an invalid ORCID
+ throw new \InvalidArgumentException(__d('orcid_source', 'error.search', [$response->getStatusCode()]));
+ }
+
+ if($response->getStatusCode() != HttpStatusCodesEnum::HTTP_OK) {
+ // This is probably an RDF blob, which is slightly annoying to parse.
+ // Rather than do it properly since we don't parse RDF anywhere else,
+ // we return a generic error.
+ throw new \RuntimeException(__d('orcid_source', 'error.search', [$response->getStatusCode()]));
+ }
+
+ return $response->getJson();
+ }
+
+ /**
+ * Get the root URL for the ORCID API.
+ *
+ * @param string $api API type: auth, public, or member
+ * @param string $tier API tier: prod or sandbox
+ * @return string URL prefix
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function orcidUrl(string $api=OrcidSourceApiEnum::PUBLIC, string $tier=OrcidSourceTierEnum::PROD): string
+ {
+ $orcidUrls = [
+ OrcidSourceApiEnum::AUTH => [
+ OrcidSourceTierEnum::PROD => 'https://orcid.org',
+ OrcidSourceTierEnum::SANDBOX => 'https://sandbox.orcid.org'
+ ],
+ OrcidSourceApiEnum::MEMBERS => [
+ OrcidSourceTierEnum::PROD => 'https://api.orcid.org',
+ OrcidSourceTierEnum::SANDBOX => 'https://api.sandbox.orcid.org'
+ ],
+ OrcidSourceApiEnum::PUBLIC => [
+ OrcidSourceTierEnum::PROD => 'https://pub.orcid.org',
+ OrcidSourceTierEnum::SANDBOX => 'https://pub.sandbox.orcid.org'
+ ]
+ ];
+
+ return $orcidUrls[$api][$tier];
+ }
+
+
+ /**
+ * Establish connection to ORCID API by configuring the HTTP client with appropriate credentials.
+ *
+ * @param ExternalIdentitySource $exterrnalIdentitySource
+ * @param string|null $orcidIdentifier The ORCID identifier to use for authentication
+ * @return Client Configured HTTP client for ORCID API requests
+ * @since COmanage Registry v5.2.0
+ */
+ protected function orcidConnect(
+ \App\Model\Entity\ExternalIdentitySource $exterrnalIdentitySource,
+ ?string $orcidIdentifier = null
+ ): \Cake\Http\Client {
+ $this->orcidSource = $this->find()
+ ->contain([
+ 'Servers' => ['Oauth2Servers'],
+ 'ExternalIdentitySources',
+ ])
+ ->innerJoinWith('Servers.Oauth2Servers')
+ ->innerJoinWith('ExternalIdentitySources')
+ ->where([
+ 'Servers.plugin' => 'CoreServer.Oauth2Servers',
+ 'ExternalIdentitySources.id' => $exterrnalIdentitySource->id,
+ 'ExternalIdentitySources.plugin' => 'OrcidSource.OrcidSources',
+ ])
+ ->first();
+
+ // Set the CO ID
+ $this->setCurCoId($this->orcidSource->server->co_id);
+
+ if (empty($this->orcidSource->id)) {
+ throw new \InvalidArgumentException(__d('error', 'notfound', [__d('core_server', 'controller.Oauth2Servers')]));
+ }
+
+ // Since this is null, we will use the master access token stored in Oauth2Server Configuration
+ if ($this->orcidSource->api_type !== OrcidSourceApiEnum::PUBLIC) {
+ $this->orcidToken = $this->orcidTokensTable
+ ->find()
+ ->where([
+ 'orcid_source_id' => $this->orcidSource->id,
+ 'orcid_identifier' => $orcidIdentifier,
+ ])
+ ->first();
+
+ if (empty($this->orcidToken->access_token)) {
+ throw new \InvalidArgumentException(__d('orcid_source', 'error.token.none'));
+ }
+ }
+
+ return $this->oauth2ServersTable->createHttpClient($this->orcidSource->server->oauth2_server->id);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ * @throws InvalidArgumentException
+ * @throws RecordNotFoundException
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ foreach([
+ 'external_source_identity_id',
+ 'default_affiliation_type_id',
+ 'address_type_id',
+ 'email_address_type_id',
+ 'name_type_id',
+ 'telephone_number_type_id'
+ ] as $field) {
+ $validator->add($field, [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString($field);
+ }
+
+ $validator->add('server_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->allowEmptyString('server_id');
+
+ $validator->add('api_tier', [
+ 'content' => ['rule' => ['inList', OrcidSourceTierEnum::getConstValues()]]
+ ]);
+ $validator->allowEmptyString('api_tier');
+
+ $validator->add('api_type', [
+ 'content' => ['rule' => ['inList', OrcidSourceApiEnum::getConstValues()]]
+ ]);
+ $validator->allowEmptyString('api_type');
+
+ $validator->add('scope_inherit', [
+ 'content' => ['rule' => 'boolean']
+ ]);
+ $validator->allowEmptyString('scope_inherit');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/OrcidSource/src/Model/Table/OrcidTokensTable.php b/app/plugins/OrcidSource/src/Model/Table/OrcidTokensTable.php
new file mode 100644
index 000000000..6db305dc1
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Table/OrcidTokensTable.php
@@ -0,0 +1,192 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ // Define associations
+ $this->belongsTo('OrcidSource.OrcidSources');
+ $this->setDisplayField('orcid_identifier');
+ $this->setPrimaryLink('orcid_source_id');
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false, //['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Perform actions while marshaling data, before validation.
+ *
+ * @param EventInterface $event Event
+ * @param \ArrayObject $data Object data, in array format
+ * @param \ArrayObject $options Entity save options
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function beforeMarshal(EventInterface $event, \ArrayObject $data, \ArrayObject $options)
+ {
+ // Encryption logic
+ $key = Security::getSalt();
+
+ foreach (['id_token', 'access_token', 'refresh_token'] as $column) {
+ if (!empty($data[$column])) {
+ // Security::encrypt expects string, $key must be correct length for the cipher!
+ $payload = base64_encode(Security::encrypt($data[$column], $key));
+
+ // If updating, try to fetch existing stored value to compare
+ $stored_key = '';
+ if (!empty($data['id'])) {
+ $entity = $this->find()->select([$column])->where(['id' => $data['id']])->first();
+ if ($entity) {
+ $stored_key = $entity->{$column};
+ }
+ }
+
+ if ($stored_key !== $payload) {
+ $data[$column] = $payload;
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Define business rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param RulesChecker $rules RulesChecker object
+ * @return RulesChecker
+ */
+
+ public function buildRules(RulesChecker $rules): RulesChecker {
+ $rules->add(
+ $rules->isUnique(
+ ["orcid_source_id", "orcid_identifier"]),
+ __d('orcid_source', 'error.exists')
+ );
+
+ return $rules;
+ }
+
+ /**
+ * Unencrypt a value previously encrypted using salt
+ *
+ * @param string $value
+ *
+ * @return false|string
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function getUnencrypted(string $value): string|false
+ {
+ if(empty($value)) {
+ return '';
+ }
+ return Security::decrypt(base64_decode($value), Security::getSalt());
+ }
+
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ * @throws InvalidArgumentException
+ * @throws RecordNotFoundException
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('orcid_source_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('orcid_source_id');
+
+ foreach(['orcid_identifier', 'access_token', 'id_token', 'refresh_token'] as $column) {
+ $validator->add($column, [
+ 'content' => [
+ 'rule' => 'validateNotBlank',
+ 'provider' => 'table'
+ ]
+ ]);
+ }
+ $validator->notEmptyString('orcid_identifier');
+ $validator->notEmptyString('access_token');
+ $validator->allowEmptyString('id_token');
+ $validator->allowEmptyString('refresh_token');
+
+ return $validator;
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/OrcidSource/src/Model/Table/PetitionOrcidsTable.php b/app/plugins/OrcidSource/src/Model/Table/PetitionOrcidsTable.php
new file mode 100644
index 000000000..5d61ab95b
--- /dev/null
+++ b/app/plugins/OrcidSource/src/Model/Table/PetitionOrcidsTable.php
@@ -0,0 +1,156 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Artifact);
+
+ // Define associations
+ $this->belongsTo('Petitions');
+ $this->belongsTo('OrcidSource.OrcidSourceCollectors');
+
+ $this->setDisplayField('orcid_token');
+
+ $this->setPrimaryLink('petition_id');
+ $this->setRequiresCO(true);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false,
+ 'edit' => false,
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false,
+ 'index' => false
+ ]
+ ]);
+ }
+
+ /**
+ * Record an ORCID Token.
+ *
+ * @param int $petitionId Petition ID
+ * @param int $enrollmentFlowStepId Enrollment Flow Step ID
+ * @param int $orcidSourceCollectorId ORCID Source Collector ID
+ * @param string $orcid_token ORCID Token Response serialized
+ * @return void
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function record(
+ int $petitionId,
+ int $enrollmentFlowStepId,
+ int $orcidSourceCollectorId,
+ string $orcidToken,
+ ): void {
+ // Record the Identifier. We use upsert since at least initially we only support
+ // one Identifier per Petition.
+
+ $orcid = unserialize($orcidToken);
+
+ $orcidData = [
+ 'petition_id' => $petitionId,
+ 'orcid_token' => $orcidToken,
+ 'orcid_identifier' => $orcid->orcid,
+ 'orcid_source_collector_id' => $orcidSourceCollectorId,
+ ];
+
+ $this->upsertOrFail(
+ data: $orcidData,
+ whereClause: ['petition_id' => $petitionId, 'orcid_identifier' => $orcid->orcid, 'orcid_source_collector_id' => $orcidSourceCollectorId],
+ );
+
+ // Record PetitionHistory
+ $PetitionHistoryRecords = TableRegistry::getTableLocator()->get('PetitionHistoryRecords');
+ $PetitionHistoryRecords->record(
+ petitionId: $petitionId,
+ enrollmentFlowStepId: $enrollmentFlowStepId,
+ action: PetitionActionEnum::AttributesUpdated,
+ comment: __d('orcid_source', 'result.OrcidSourceCollector.collected', [$orcid->orcid])
+// We don't have $actorPersonId yet...
+// ?int $actorPersonId=null
+ );
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('orcid_source_collector_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('orcid_source_collector_id');
+
+ $validator->add('petition_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('petition_id');
+
+ $this->registerStringValidation($validator, $schema, 'orcid_token', true);
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/OrcidSource/src/OrcidSourcePlugin.php b/app/plugins/OrcidSource/src/OrcidSourcePlugin.php
new file mode 100644
index 000000000..f6fcc78d5
--- /dev/null
+++ b/app/plugins/OrcidSource/src/OrcidSourcePlugin.php
@@ -0,0 +1,93 @@
+plugin(
+ 'OrcidSource',
+ ['path' => '/orcid-source'],
+ function (RouteBuilder $builder) {
+ // Add custom routes here
+
+ $builder->fallbacks();
+ }
+ );
+ parent::routes($routes);
+ }
+
+ /**
+ * Add middleware for the plugin.
+ *
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
+ * @return \Cake\Http\MiddlewareQueue
+ */
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Add your middlewares here
+
+ return $middlewareQueue;
+ }
+
+ /**
+ * Add commands for the plugin.
+ *
+ * @param \Cake\Console\CommandCollection $commands The command collection to update.
+ * @return \Cake\Console\CommandCollection
+ */
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ // Add your commands here
+
+ $commands = parent::console($commands);
+
+ return $commands;
+ }
+
+ /**
+ * Register application container services.
+ *
+ * @param \Cake\Core\ContainerInterface $container The Container to update.
+ * @return void
+ * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
+ */
+ public function services(ContainerInterface $container): void
+ {
+ // Add your services here
+ }
+}
diff --git a/app/plugins/OrcidSource/src/View/Cell/OrcidSourceCollectorsCell.php b/app/plugins/OrcidSource/src/View/Cell/OrcidSourceCollectorsCell.php
new file mode 100644
index 000000000..788b3c14d
--- /dev/null
+++ b/app/plugins/OrcidSource/src/View/Cell/OrcidSourceCollectorsCell.php
@@ -0,0 +1,98 @@
+
+ */
+ protected array $_validCellOptions = [
+ 'vv_obj',
+ 'vv_step',
+ 'viewVars',
+ ];
+
+ /**
+ * Initialization logic run at the end of object construction.
+ *
+ * @return void
+ */
+ public function initialize(): void
+ {
+ }
+
+ /**
+ * Default display method.
+ *
+ * @param int $petitionId
+ * @return void
+ * @since COmanage Registry v5.2.0
+ */
+ public function display(int $petitionId): void
+ {
+ $vv_oi = $this->fetchTable('CoreSource.PetitionOrcids')
+ ->find()
+ ->where(['petition_id' => $this->vv_obj->id])
+ ->first();
+
+ $this->set('vv_orcid', $vv_oi);;
+
+ $this->set('vv_step', $this->vv_step);
+ $this->set('vv_obj', $this->vv_obj);
+ }
+}
diff --git a/app/plugins/OrcidSource/templates/OrcidSourceCollectors/dispatch.inc b/app/plugins/OrcidSource/templates/OrcidSourceCollectors/dispatch.inc
new file mode 100644
index 000000000..c941ef0dd
--- /dev/null
+++ b/app/plugins/OrcidSource/templates/OrcidSourceCollectors/dispatch.inc
@@ -0,0 +1,47 @@
+Html->css('OrcidSource/orcid-source', ['block' => true]);
+
+// Authenticate and fetch the token
+if (empty($vv_orcid) || empty($vv_token)) {
+ print $this->element('OrcidSource.authenticate');
+ return;
+}
+
+// Make the Form fields editable and the form submittable
+$this->Field->enableFormEditMode();
+print $this->element('OrcidSource.preview');
+
diff --git a/app/plugins/OrcidSource/templates/OrcidSourceCollectors/fields.inc b/app/plugins/OrcidSource/templates/OrcidSourceCollectors/fields.inc
new file mode 100644
index 000000000..b7f871854
--- /dev/null
+++ b/app/plugins/OrcidSource/templates/OrcidSourceCollectors/fields.inc
@@ -0,0 +1,40 @@
+ [
+ 'fieldLabel' => __d('orcid_source', 'controller.OrcidSources', [1])
+ ]
+];
+
+$subnav = [
+ 'tabs' => ['EnrollmentFlowSteps', 'OrcidSource.OrcidSourceCollectors'],
+ 'action' => [
+ 'EnrollmentFlowSteps' => ['edit', 'view'],
+ 'OrcidSource.OrcidSourceCollectors' => ['edit']
+ ],
+];
diff --git a/app/plugins/OrcidSource/templates/OrcidSources/fields.inc b/app/plugins/OrcidSource/templates/OrcidSources/fields.inc
new file mode 100644
index 000000000..f1ff2572e
--- /dev/null
+++ b/app/plugins/OrcidSource/templates/OrcidSources/fields.inc
@@ -0,0 +1,78 @@
+scope_inherit, FILTER_VALIDATE_BOOLEAN)) {
+ $vv_inherited_scopes = $vv_obj->server?->oauth2_server?->scope ?? OrcidSourceScopeEnum::DEFAULT_SCOPE;
+}
+
+$fields = [
+ // Render the Redirect URI
+ 'redirect_uri' => [
+ 'readonly' => true,
+ 'default' => $vv_redirect_uri
+ ],
+ 'server_id' => [
+ 'empty' => false,
+ 'required' => true
+ ],
+ 'api_type' => [
+ 'options' => $api_types,
+ 'type' => 'select',
+ 'empty' => false,
+ 'required' => true
+ ],
+ 'api_tier' => [
+ 'options' => $api_tiers,
+ 'type' => 'select',
+ 'empty' => false,
+ 'required' => true
+ ],
+ 'scope_inherit',
+ // Render active scopes
+ 'Scope' => [
+ 'readonly' => true,
+ ],
+ 'SUBTITLE' => [
+ 'subtitle' => __d('orcid_source', 'information.OrcidSources.default.types')
+ ],
+ 'address_type_id',
+ 'default_affiliation_type_id',
+ 'email_address_type_id',
+ 'name_type_id'
+];
+
+$subnav = [
+ 'tabs' => ['ExternalIdentitySources', 'OrcidSource.OrcidSources', 'ExternalIdentitySources@action.search'],
+ 'action' => [
+ 'ExternalIdentitySources' => ['edit', 'view', 'search'],
+ 'OrcidSource.OrcidSources' => ['edit'],
+ 'ExternalIdentitySources@action.search' => [],
+ ],
+];
diff --git a/app/plugins/OrcidSource/templates/cell/OrcidSourceCollectors/display.php b/app/plugins/OrcidSource/templates/cell/OrcidSourceCollectors/display.php
new file mode 100644
index 000000000..6347ade5d
--- /dev/null
+++ b/app/plugins/OrcidSource/templates/cell/OrcidSourceCollectors/display.php
@@ -0,0 +1,38 @@
+
+
+orcid_identifier)): ?>
+
+
+ = __d('orcid_source', 'result.OrcidSourceCollector.collected', [$vv_orcid->orcid_identifier]) ?>
+
+
+
diff --git a/app/plugins/OrcidSource/templates/element/authenticate.php b/app/plugins/OrcidSource/templates/element/authenticate.php
new file mode 100644
index 000000000..6d58c1dec
--- /dev/null
+++ b/app/plugins/OrcidSource/templates/element/authenticate.php
@@ -0,0 +1,51 @@
+login'
+ . __d('orcid_source', 'information.OrcidSourceCollectors.authenticate');
+
+print $this->Form->hidden('op', ['value' => 'authenticate']);
+
+?>
+
+
+ = $this->Html->image('OrcidSource.orcid_128x128.png', ['alt' => 'Logo', 'class' => 'mb-3']) ?>
+
= __d('orcid_source', 'information.OrcidSourceCollectors.authenticate') ?>
+
= __d('orcid_source', 'information.OrcidSourceCollectors.sign_in') ?>
+ = $this->Form->button(
+ $btnAuthenticateLabel,
+ [
+ 'id' => 'orcid-auth-btn',
+ 'escapeTitle' => false,
+ 'type' => 'submit',
+ 'class' => 'spin submit-button btn btn-primary d-flex mx-auto',
+ ]
+ )
+ ?>
+
diff --git a/app/plugins/OrcidSource/templates/element/preview.php b/app/plugins/OrcidSource/templates/element/preview.php
new file mode 100644
index 000000000..c5ad8ab0b
--- /dev/null
+++ b/app/plugins/OrcidSource/templates/element/preview.php
@@ -0,0 +1,50 @@
+Form->hidden('op', ['value' => 'savetoken']);
+print $this->Form->hidden('orcid_token', ['value' => serialize($vv_token)]);
+
+?>
+
+
+
+
+ = __d('field', 'identifier') ?>
+ = __d('field', 'value') ?>
+
+
+
+
+
+ = __d('orcid_source', 'information.orcid_source.identifier') ?>
+ = $vv_orcid ?>
+
+
+
+
diff --git a/app/plugins/OrcidSource/tests/bootstrap.php b/app/plugins/OrcidSource/tests/bootstrap.php
new file mode 100644
index 000000000..a3cd830d9
--- /dev/null
+++ b/app/plugins/OrcidSource/tests/bootstrap.php
@@ -0,0 +1,55 @@
+loadSqlFiles('tests/schema.sql', 'test');
diff --git a/app/plugins/OrcidSource/tests/schema.sql b/app/plugins/OrcidSource/tests/schema.sql
new file mode 100644
index 000000000..d28524851
--- /dev/null
+++ b/app/plugins/OrcidSource/tests/schema.sql
@@ -0,0 +1 @@
+-- Test database schema for OrcidSource
diff --git a/app/plugins/OrcidSource/webroot/.gitkeep b/app/plugins/OrcidSource/webroot/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/plugins/OrcidSource/webroot/css/orcid-source.css b/app/plugins/OrcidSource/webroot/css/orcid-source.css
new file mode 100644
index 000000000..4b8fc336c
--- /dev/null
+++ b/app/plugins/OrcidSource/webroot/css/orcid-source.css
@@ -0,0 +1,18 @@
+.text-center img {
+ width: 72px;
+}
+
+.text-center h2 {
+ color: #68b245;
+}
+
+#orcid-auth-btn {
+ color: white;
+ font-weight: bolder;
+}
+
+.material-symbols-outlined {
+ font-size: 22px;
+ vertical-align: middle;
+}
+
diff --git a/app/plugins/OrcidSource/webroot/img/orcid_128x128.png b/app/plugins/OrcidSource/webroot/img/orcid_128x128.png
new file mode 100644
index 000000000..484207317
Binary files /dev/null and b/app/plugins/OrcidSource/webroot/img/orcid_128x128.png differ
diff --git a/app/plugins/SshKeyAuthenticator/README.md b/app/plugins/SshKeyAuthenticator/README.md
new file mode 100644
index 000000000..f73123136
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/README.md
@@ -0,0 +1,11 @@
+# SshKeyAuthenticator plugin for CakePHP
+
+## Installation
+
+You can install this plugin into your CakePHP application using [composer](https://getcomposer.org).
+
+The recommended way to install composer packages is:
+
+```
+composer require your-name-here/ssh-key-authenticator
+```
diff --git a/app/plugins/SshKeyAuthenticator/composer.json b/app/plugins/SshKeyAuthenticator/composer.json
new file mode 100644
index 000000000..c8e124adb
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "your-name-here/ssh-key-authenticator",
+ "description": "SshKeyAuthenticator plugin for CakePHP",
+ "type": "cakephp-plugin",
+ "license": "MIT",
+ "require": {
+ "php": ">=7.2",
+ "cakephp/cakephp": "4.6.*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "SshKeyAuthenticator\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "SshKeyAuthenticator\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
+ }
+ }
+}
diff --git a/app/plugins/SshKeyAuthenticator/config/plugin.json b/app/plugins/SshKeyAuthenticator/config/plugin.json
new file mode 100644
index 000000000..80f8350de
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/config/plugin.json
@@ -0,0 +1,34 @@
+{
+ "types": {
+ "authenticator": [
+ "SshKeyAuthenticators"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "ssh_key_authenticators": {
+ "columns": {
+ "id": {},
+ "authenticator_id": {}
+ },
+ "indexes": {
+ "ssh_key_authenticators_i1": { "columns": [ "authenticator_id" ]}
+ }
+ },
+ "ssh_keys": {
+ "columns": {
+ "id": {},
+ "ssh_key_authenticator_id": { "type": "integer", "foreignkey": { "table": "ssh_key_authenticators", "column": "id" }, "notnull": true },
+ "person_id": {},
+ "skey": { "type": "text" },
+ "comment": {},
+ "type": { "type": "string", "size": 32 }
+ },
+ "indexes": {
+ "ssh_keys_i1": { "columns": [ "ssh_key_authenticator_id" ]},
+ "ssh_keys_i2": { "columns": [ "person_id" ]}
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/SshKeyAuthenticator/phpunit.xml.dist b/app/plugins/SshKeyAuthenticator/phpunit.xml.dist
new file mode 100644
index 000000000..3f44f9113
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/phpunit.xml.dist
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+
+ src/
+
+
+
diff --git a/app/plugins/SshKeyAuthenticator/resources/locales/en_US/ssh_key_authenticator.po b/app/plugins/SshKeyAuthenticator/resources/locales/en_US/ssh_key_authenticator.po
new file mode 100644
index 000000000..3881d4f02
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/resources/locales/en_US/ssh_key_authenticator.po
@@ -0,0 +1,77 @@
+# COmanage Registry Localizations (ssh_key_authenticator domain)
+#
+# Portions licensed to the University Corporation for Advanced Internet
+# Development, Inc. ("UCAID") under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information
+# regarding copyright ownership.
+#
+# UCAID licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# @link https://www.internet2.edu/comanage COmanage Project
+# @package registry-plugins
+# @since COmanage Registry v5.2.0
+# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+msgid "controller.SshKeyAuthenticators"
+msgstr "{0,plural,=1{SSH Key Authenticator} other{SSH Key Authenticators}}"
+
+msgid "controller.SshKeys"
+msgstr "{0,plural,=1{SSH Key} other{SSH Keys}}"
+
+msgid "enumeration.SshKeyActionEnum.SSHD"
+msgstr "SSH Key Deleted"
+
+msgid "enumeration.SshKeyActionEnum.SSHU"
+msgstr "SSH Key Uploaded"
+
+msgid "enumeration.SshKeyTypeEnum.ecdsa-sha2-nistp256"
+msgstr "ECDSA"
+
+msgid "enumeration.SshKeyTypeEnum.ecdsa-sha2-nistp384"
+msgstr "ECDSA384"
+
+msgid "enumeration.SshKeyTypeEnum.ecdsa-sha2-nistp521"
+msgstr "ECDSA521"
+
+msgid "enumeration.SshKeyTypeEnum.ssh-dss"
+msgstr "DSA"
+
+msgid "enumeration.SshKeyTypeEnum.ssh-ed25519"
+msgstr "ED25519"
+
+msgid "enumeration.SshKeyTypeEnum.ssh-rsa"
+msgstr "RSA"
+
+msgid "enumeration.SshKeyTypeEnum.ssh-rsa"
+msgstr "RSA1"
+
+msgid "error.SshKeys.empty"
+msgstr "SSH Key file was empty"
+
+msgid "error.SshKeys.format"
+msgstr "File does not appear to be a valid ssh public key"
+
+msgid "error.SshKeys.private"
+msgstr "Uploaded file appears to be a private key"
+
+msgid "field.keyFile"
+msgstr "Select an SSH Public Key file to upload"
+
+msgid "operation.upload"
+msgstr "Upload a New SSH Key"
+
+msgid "result.registered"
+msgstr "{0} {1} registered"
+
+msgid "result.uploaded"
+msgstr "SSH Key {0} uploaded"
diff --git a/app/plugins/SshKeyAuthenticator/src/Controller/AppController.php b/app/plugins/SshKeyAuthenticator/src/Controller/AppController.php
new file mode 100644
index 000000000..97b0e1aa5
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+ [
+ 'SshKeyAuthenticators.id' => 'asc'
+ ]
+ ];
+
+
+ /**
+ * Callback run prior to the request render.
+ *
+ * @param EventInterface $event Cake Event
+ *
+ * @return Response|void
+ * @since COmanage Registry v5.0.0
+ */
+
+ public function beforeRender(EventInterface $event) {
+ $link = $this->getPrimaryLink(true);
+
+ if(!empty($link->value)) {
+ $this->set('vv_bc_parent_obj', $this->SshKeyAuthenticators->Authenticators->get($link->value));
+ $this->set('vv_bc_parent_displayfield', $this->SshKeyAuthenticators->Authenticators->getDisplayField());
+ $this->set('vv_bc_parent_primarykey', $this->SshKeyAuthenticators->Authenticators->getPrimaryKey());
+ }
+
+ return parent::beforeRender($event);
+ }
+
+}
diff --git a/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php b/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php
new file mode 100644
index 000000000..b9200e223
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php
@@ -0,0 +1,94 @@
+ [
+ 'SshKeys.comment' => 'asc'
+ ]
+ ];
+
+ /**
+ * Handle an add action for an SSH Key.
+ *
+ * @since COmanage Registry v5.2.0
+ */
+
+ public function add() {
+ $obj = $this->SshKeys->newEmptyEntity();
+
+ if(empty($this->requestParam('person_id'))) {
+ throw new \InvalidArgumentException(__d('error', 'notprov', [__d('controller', 'People', [1])]));
+ }
+
+ if($this->request->is('post')) {
+ try {
+ $upload = $this->getRequest()->getData('keyFile')->getStream()->getContents();
+
+ $obj = $this->SshKeys->addFromKeyFile(
+ sshKeyAuthenticatorId: (int)$this->requestParam('ssh_key_authenticator_id'),
+ personId: (int)$this->requestParam('person_id'),
+ contents: $upload
+ );
+
+ return $this->generateRedirect($obj);
+ }
+ catch(\Exception $e) {
+ // This throws \Cake\ORM\Exception\RolledbackTransactionException if
+ // aborted in afterSave
+
+ $this->Flash->error($e->getMessage());
+ }
+ }
+
+ // Pass $obj as context so the view can render validation errors
+ $this->set('vv_obj', $obj);
+
+ // Default title is add new object
+ [$title, $supertitle, $subtitle] = StringUtilities::entityAndActionToTitle(
+ $obj,
+ 'SshKeys',
+ 'add',
+ 'ssh_key_authenticator'
+ );
+ $this->set('vv_title', $title);
+ $this->set('vv_supertitle', $supertitle);
+ $this->set('vv_subtitle', $subtitle);
+
+ // Let the view render
+ $this->render('/Standard/add-edit-view');
+ }
+}
diff --git a/app/plugins/SshKeyAuthenticator/src/Lib/Enum/SshKeyActionEnum.php b/app/plugins/SshKeyAuthenticator/src/Lib/Enum/SshKeyActionEnum.php
new file mode 100644
index 000000000..20d4fad4f
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/Lib/Enum/SshKeyActionEnum.php
@@ -0,0 +1,40 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+
+ /**
+ * Determine if this entity record can be deleted.
+ *
+ * @since COmanage Registry v5.2.0
+ * @return bool True if the record can be deleted, false otherwise
+ */
+
+ public function canDelete(): bool {
+ return true;
+ }
+
+ /**
+ * Determine if this entity is Read Only.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Entity $entity Cake Entity
+ * @return boolean true if the entity is read only, false otherwise
+ */
+
+ public function isReadOnly(): bool {
+ // SSH Keys can't be altered once created (though they can be deleted)
+
+ return true;
+ }
+}
diff --git a/app/plugins/SshKeyAuthenticator/src/Model/Entity/SshKeyAuthenticator.php b/app/plugins/SshKeyAuthenticator/src/Model/Entity/SshKeyAuthenticator.php
new file mode 100644
index 000000000..b89c2ce77
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/Model/Entity/SshKeyAuthenticator.php
@@ -0,0 +1,51 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php
new file mode 100644
index 000000000..9ac019b9b
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php
@@ -0,0 +1,157 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ // Timestamp behavior handles created/modified updates
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('Authenticators');
+
+ $this->hasMany('SshKeyAuthenticator.SshKeys')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+
+ $this->setDisplayField('id');
+
+ $this->setPrimaryLink('authenticator_id');
+ $this->setRequiresCO(true);
+
+ $this->setEditContains([
+ 'Authenticators',
+ ]);
+
+ $this->setViewContains([
+ 'Authenticators',
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false, // Delete the pluggable object instead
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Table specific logic to generate a display field.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param \SshKeyAuthenticator\Model\Entity\SshKeyAuthenticator $entity Entity to generate display field for
+ * @return string Display field
+ */
+ public function generateDisplayField(\SshKeyAuthenticator\Model\Entity\SshKeyAuthenticator $entity): string {
+ return $entity->authenticator->description;
+ }
+
+ /**
+ * Assemble Authenticator data for provisioning.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Authenticator $cfg Authenticator Configuration
+ * @param int $personId Person ID
+ * @return array Array of SshKey entities
+ */
+
+ public function marshalProvisioningData(
+ \App\Model\Entity\Authenticator $cfg,
+ int $personId
+ ): array {
+ // Retrieve any Passwords associated with this Person and the requested configuration.
+ // We'll include all available Password types (encodings) since we don't know which types
+ // any specific Provisioner will be interested in.
+
+ $sshKeys = $this->SshKeys->find()
+ ->where([
+ 'SshKeys.person_id' => $personId,
+ 'SshKeys.ssh_key_authenticator_id' => $cfg->ssh_key_authenticator->id
+ ])
+ ->all();
+
+ return $sshKeys->toArray();
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('authenticator_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('authenticator_id');
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeysTable.php b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeysTable.php
new file mode 100644
index 000000000..4a3877abc
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeysTable.php
@@ -0,0 +1,329 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ // Timestamp behavior handles created/modified updates
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Secondary);
+
+ // Define associations
+ $this->belongsTo('SshKeyAuthenticator.SshKeyAuthenticators');
+ $this->belongsTo('People');
+
+ $this->setDisplayField('comment');
+
+ $this->setPrimaryLink('SshKeyAuthenticator.ssh_key_authenticator_id');
+ $this->setRequiresCO(true);
+ $this->setRedirectGoal('index');
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => ['platformAdmin', 'coAdmin'],
+ // Following the v4 pattern, SSH Keys cannot be edited
+ 'edit' => false,
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that are permitted on readonly entities (besides view)
+ // SSH Entities are readOnly but permit delete, so we need to re-enable the action
+ 'readOnly' => ['delete'],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => ['platformAdmin', 'coAdmin'],
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Add an SSH Key from a string parsed from a file.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param int $sshKeyAuthenticatorId SSH Key Authenticator ID
+ * @param int $personId Person ID
+ * @param string $contents Contents of SSH key file (not parsed)
+ */
+
+ public function addFromKeyFile(
+ int $sshKeyAuthenticatorId,
+ int $personId,
+ string $contents
+ ): SshKey {
+ // GMR-2 will handle checking that $personId and $sshKeyAuthenticatorId are in the
+ // same CO, so we don't have to.
+
+ // Process the key file
+ $keyFileString = rtrim($contents);
+
+ if(empty($keyFileString) || ctype_space($keyFileString)) {
+ throw new \InvalidArgumentException(__d('ssh_key_authenticator', 'error.SshKeys.empty'));
+ }
+
+ if(preg_match("/-----BEGIN.*PRIVATE.*/", $keyFileString) == 1) {
+ // This is the private key, not the public key
+ throw new \InvalidArgumentException(__d('ssh_key_authenticator', 'error.SshKeys.private'));
+ }
+
+ // We currently only support OpenSSH format, which is a triple of type/key/comment,
+ // and RFC 4716 Secure Shell (SSH) Public Key File format.
+
+ $newEntityData = [
+ 'ssh_key_authenticator_id' => $sshKeyAuthenticatorId,
+ 'person_id' => $personId
+ ];
+
+ // Currently, we only support one key per file regardless of format.
+
+ if(preg_match("/---- BEGIN SSH2 PUBLIC KEY ----.*/", $keyFileString) == 1) {
+ // RFC4716 format
+ $newEntityData = array_merge($newEntityData, $this->parseRfc4716($keyFileString));
+ } else {
+ // OpenSSH format
+
+ $sshKeyLine = explode("\n", $keyFileString);
+ $bits = explode(' ', $sshKeyLine[0], 3);
+
+ $newEntityData['type'] = $bits[0];
+ $newEntityData['skey'] = $bits[1];
+ $newEntityData['comment'] = $bits[2];
+ }
+
+ if(empty($newEntityData['skey'])) {
+ throw new \InvalidArgumentException(__d('ssh_key_authenticator', 'error.SshKeys.private'));
+ }
+
+ $sshkey = $this->newEntity($newEntityData);
+
+ $this->saveOrFail($sshkey);
+
+ // Record History and trigger provisioning
+
+ $this->People->recordHistory(
+ $sshkey,
+ SshKeyActionEnum::SshKeyUploaded,
+ __d('ssh_key_authenticator', 'result.uploaded', [$sshkey->comment])
+ );
+
+ $this->People->requestProvisioning($personId, ProvisioningContextEnum::Automatic);
+
+ return $sshkey;
+ }
+
+ /**
+ * Parse an RFC 4716 SSH Public Key File.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param string $keyFileString SSH Key File contents
+ * @return array Array of 'type', 'skey', and 'comment'
+ */
+
+ public function parseRfc4716(string $keyFileString) {
+ // RFC 4716 format is line based.
+ $lines = explode("\n", $keyFileString);
+
+ $firstLineFound = false;
+ $keyFileHeaders = [];
+ $lineContinuationInProgress = false;
+ $base64EncodedBody = "";
+
+ foreach ($lines as $line) {
+ // A Conforming key file would begin immediately with the begin marker
+ // but try to be liberal in what we accept so skip any initial lines.
+ if(!$firstLineFound) {
+ if(preg_match("/^---- BEGIN SSH2 PUBLIC KEY ----.*/", $line) === 1) {
+ $firstLineFound = true;
+ }
+ continue;
+ }
+
+ // Parse key file headers with possible continuation lines
+ // until the base64-encoded body begins.
+ if(empty($base64EncodedBody)) {
+ if(!$lineContinuationInProgress) {
+ $headerLineParts = preg_split("/:/u", $line, 2);
+ if(count($headerLineParts) == 1) {
+ $base64EncodedBody .= $line;
+ } elseif(count($headerLineParts) == 2) {
+ $headerTag = $headerLineParts[0];
+ if(substr($headerLineParts[1], -1) == '\\') {
+ $lineContinuationInProgress = true;
+ $headerValue = substr($headerLineParts[1], 0, -1);
+ } else {
+ $headerValue = $headerLineParts[1];
+ $keyFileHeaders[$headerTag] = $headerValue;
+ }
+ continue;
+ }
+ } else {
+ if(substr($line, -1) == '\\') {
+ $headerValue = $headerValue . substr($line, 0, -1);
+ } else {
+ $headerValue = $headerValue . $line;
+ $lineContinuationInProgress = false;
+ $keyFileHeaders[$headerTag] = $headerValue;
+ }
+ continue;
+ }
+ } else {
+ // Stop parsing when we find the end marker and so ignore any
+ // non-conforming end material.
+ if(preg_match("/^---- END SSH2 PUBLIC KEY ----.*/", $line) === 1) {
+ break;
+ }
+ $base64EncodedBody .= $line;
+ continue;
+ }
+ }
+
+ // Base-64 decode the body. The resulting binary string has the format
+ // 3 null bytes, key type string, 3 null bytes, public key. The key type
+ // string needs to further be trimmed to remove non-ascii characters.
+ $bodyBinaryString = base64_decode($base64EncodedBody);
+ $keyTypeString = trim(explode("\x00\x00\x00", $bodyBinaryString)[1], "\x00..\x1F");
+
+ // An empty comment is allowed.
+ $comment = "";
+
+ if(array_key_exists("Comment", $keyFileHeaders)) {
+ $comment = trim($keyFileHeaders["Comment"]);
+ } elseif (array_key_exists("Subject", $keyFileHeaders)) {
+ $comment = trim($keyFileHeaders["Subject"]);
+ }
+
+ return [
+ 'type' => $keyTypeString,
+ 'skey' => $base64EncodedBody,
+ 'comment' => $comment
+ ];
+ }
+
+ /**
+ * Obtain the current Authenticator status for a Person.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Authenticator $cfg Authenticator Configuration
+ * @param int $personId Person ID
+ * @return array Array with values
+ * status: AuthenticatorStatusEnum
+ * comment: Human readable string, visible to the CO Person
+ */
+
+ public function status(\App\Model\Entity\Authenticator $cfg, int $personId): array {
+ // Are there any SSH Keys for this person?
+
+ $count = $this->find()
+ ->where([
+ 'ssh_key_authenticator_id' => $cfg->ssh_key_authenticator->id,
+ 'person_id' => $personId
+ ])
+ ->count();
+
+ if($count > 0) {
+ return [
+ 'status' => AuthenticatorStatusEnum::Active,
+ 'comment' => __d('ssh_key_authenticator', 'result.registered', [$count, __d('ssh_key_authenticator', 'controller.SshKeys', [$count])])
+ ];
+ }
+
+ return [
+ 'status' => AuthenticatorStatusEnum::NotSet,
+ 'comment' => __d('result', 'set.not')
+ ];
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('ssh_key_authenticator_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('ssh_keyauthenticator_id');
+
+ $validator->add('person_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('person_id');
+
+ $validator->add('skey', [
+ 'filter' => ['rule' => ['validateInput'],
+ 'provider' => 'table']
+ ]);
+ $validator->notEmptyString('skey');
+
+ $this->registerStringValidation($validator, $schema, 'comment', false);
+
+ $validator->add('type', [
+ 'content' => ['rule' => ['inList', SshKeyTypeEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('type');
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/SshKeyAuthenticator/src/SshKeyAuthenticatorPlugin.php b/app/plugins/SshKeyAuthenticator/src/SshKeyAuthenticatorPlugin.php
new file mode 100644
index 000000000..d5e6fe584
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/src/SshKeyAuthenticatorPlugin.php
@@ -0,0 +1,93 @@
+plugin(
+ 'SshKeyAuthenticator',
+ ['path' => '/ssh-key-authenticator'],
+ function (RouteBuilder $builder) {
+ // Add custom routes here
+
+ $builder->fallbacks();
+ }
+ );
+ parent::routes($routes);
+ }
+
+ /**
+ * Add middleware for the plugin.
+ *
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
+ * @return \Cake\Http\MiddlewareQueue
+ */
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Add your middlewares here
+
+ return $middlewareQueue;
+ }
+
+ /**
+ * Add commands for the plugin.
+ *
+ * @param \Cake\Console\CommandCollection $commands The command collection to update.
+ * @return \Cake\Console\CommandCollection
+ */
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ // Add your commands here
+
+ $commands = parent::console($commands);
+
+ return $commands;
+ }
+
+ /**
+ * Register application container services.
+ *
+ * @param \Cake\Core\ContainerInterface $container The Container to update.
+ * @return void
+ * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
+ */
+ public function services(ContainerInterface $container): void
+ {
+ // Add your services here
+ }
+}
diff --git a/app/plugins/SshKeyAuthenticator/templates/SshKeyAuthenticators/fields.inc b/app/plugins/SshKeyAuthenticator/templates/SshKeyAuthenticators/fields.inc
new file mode 100644
index 000000000..e22c09e37
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/templates/SshKeyAuthenticators/fields.inc
@@ -0,0 +1,46 @@
+Field->disableFormEditMode();
+
+$subnav = [
+ 'tabs' => ['Authenticators', 'SshKeyAuthenticator.SshKeyAuthenticators'],
+ 'action' => [
+ 'Authenticators' => ['edit'],
+ 'SshKeyAuthenticator.SshKeyAuthenticators' => ['edit']
+ ]
+];
+
+// There are currently no configurable options for the SSH Key Authenticator
+$alerts = [
+ [
+ 'type' => 'information',
+ 'message' => __d('information', 'plugin.config.none')
+ ]
+];
\ No newline at end of file
diff --git a/app/plugins/SshKeyAuthenticator/templates/SshKeys/columns.inc b/app/plugins/SshKeyAuthenticator/templates/SshKeys/columns.inc
new file mode 100644
index 000000000..4f428592f
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/templates/SshKeys/columns.inc
@@ -0,0 +1,56 @@
+ [
+ 'type' => 'link'
+ ],
+ 'type' => [
+ 'type' => 'enum',
+ 'plugin' => 'SshKeyAuthenticator',
+ 'class' => 'SshKeyTypeEnum'
+ ]
+];
+
+// $topLinks appear as an upper right menu.
+// We use $topLinks to rebuild the add link because we need additional parameters.
+$suppressAddLink = true;
+
+$topLinks = [
+ [
+ 'icon' => 'upload',
+ 'order' => 'Default',
+ 'label' => __d('ssh_key_authenticator', 'operation.upload'),
+ 'link' => [
+ 'action' => 'add',
+ '?' => [
+ 'person_id' => $vv_person_id
+ ]
+ ],
+ 'class' => ''
+ ]
+];
diff --git a/app/plugins/SshKeyAuthenticator/templates/SshKeys/fields.inc b/app/plugins/SshKeyAuthenticator/templates/SshKeys/fields.inc
new file mode 100644
index 000000000..674c57e26
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/templates/SshKeys/fields.inc
@@ -0,0 +1,55 @@
+ $vv_primary_link_obj->id,
+ 'person_id' => $vv_person_id
+ ];
+
+ // As of v3.2.0, we only allow uploading of SSH Keys, not manually adding or editing
+ $fields = [
+ 'keyFile' => [
+ 'type' => 'file'
+ ]
+ ];
+} elseif($vv_action == 'view') {
+ $fields = [
+ 'type',
+ 'comment',
+ 'skey'
+ ];
+}
diff --git a/app/plugins/SshKeyAuthenticator/tests/bootstrap.php b/app/plugins/SshKeyAuthenticator/tests/bootstrap.php
new file mode 100644
index 000000000..033ab6831
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/tests/bootstrap.php
@@ -0,0 +1,55 @@
+loadSqlFiles('tests/schema.sql', 'test');
diff --git a/app/plugins/SshKeyAuthenticator/tests/schema.sql b/app/plugins/SshKeyAuthenticator/tests/schema.sql
new file mode 100644
index 000000000..024cfa376
--- /dev/null
+++ b/app/plugins/SshKeyAuthenticator/tests/schema.sql
@@ -0,0 +1 @@
+-- Test database schema for SshKeyAuthenticator
diff --git a/app/plugins/SshKeyAuthenticator/webroot/.gitkeep b/app/plugins/SshKeyAuthenticator/webroot/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/plugins/TermsAgreer/config/plugin.json b/app/plugins/TermsAgreer/config/plugin.json
new file mode 100644
index 000000000..90cac19ad
--- /dev/null
+++ b/app/plugins/TermsAgreer/config/plugin.json
@@ -0,0 +1,35 @@
+{
+ "types": {
+ "enrollment_flow_step": [
+ "AgreementCollectors"
+ ]
+ },
+ "schema": {
+ "tables": {
+ "agreement_collectors": {
+ "columns": {
+ "id": {},
+ "enrollment_flow_step_id": {},
+ "t_and_c_mode": { "type": "string", "size": 2 }
+ },
+ "indexes": {
+ "agreement_collectors_i1": { "columns": [ "enrollment_flow_step_id" ] }
+ }
+ },
+ "petition_agreements": {
+ "columns": {
+ "id": {},
+ "petition_id": {},
+ "agreement_collector_id": { "type": "integer", "foreignkey": { "table": "agreement_collectors", "column": "id" } },
+ "terms_and_conditions_id": { "type": "integer", "foreignkey": { "table": "terms_and_conditions", "column": "id" } },
+ "identifier": { "type": "string", "size": 512 },
+ "agreement_time": { "type": "datetime" }
+ },
+ "indexes": {
+ "petition_agreements_i1": { "columns": [ "petition_id" ] },
+ "petition_agreements_i2": { "needed": false, "columns": [ "terms_and_conditions_id" ] }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/TermsAgreer/resources/locales/en_US/terms_agreer.po b/app/plugins/TermsAgreer/resources/locales/en_US/terms_agreer.po
new file mode 100644
index 000000000..b2c8b8cda
--- /dev/null
+++ b/app/plugins/TermsAgreer/resources/locales/en_US/terms_agreer.po
@@ -0,0 +1,62 @@
+# COmanage Registry Localizations (terms_agreer domain)
+#
+# Portions licensed to the University Corporation for Advanced Internet
+# Development, Inc. ("UCAID") under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information
+# regarding copyright ownership.
+#
+# UCAID licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with the
+# License. You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# @link https://www.internet2.edu/comanage COmanage Project
+# @package registry-plugins
+# @since COmanage Registry v5.2.0
+# @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+msgid "controller.AgreementCollectors"
+msgstr "{0,plural,=1{T&C Agreement Collector} other{T&C Agreement Collectors}}"
+
+msgid "controller.PetitionAgreements"
+msgstr "{0,plural,=1{Petition T&C Agreement} other{Petition T&C Agreements}}"
+
+msgid "enumeration.TAndCEnrollmentModeEnum.EC"
+msgstr "Explicit Consent"
+
+msgid "enumeration.TAndCEnrollmentModeEnum.X"
+msgstr "Ignore"
+
+msgid "enumeration.TAndCEnrollmentModeEnum.IC"
+msgstr "Implied Consent"
+
+msgid "error.TAndCAgreement.missing"
+msgstr "Did not receive agreement for \"{0}\" (T&C {1})"
+
+msgid "field.AgreementCollectors.t_and_c_mode"
+msgstr "Terms and Conditions Mode"
+
+msgid "information.AgreementCollectors.external"
+msgstr "These Terms and Conditions will be loaded in an external browser window. After review, you must return to this window and click \"Agree\" to continue."
+
+msgid "information.AgreementCollectors.review"
+msgstr "You must review and agree to these Terms and Conditions before continuing."
+
+msgid "information.AgreementCollectors.review.tc"
+msgstr "Review Terms & Conditions"
+
+msgid "result.AgreementCollectors.ignored"
+msgstr "Terms and Conditions collection disabled"
+
+msgid "result.AgreementCollectors.recorded"
+msgstr "Agreed to Terms and Conditions {0} ({1}, {2})"
+
+msgid "result.AgreementCollectors.recorded.summary"
+msgstr "Agreed to {0} Terms and Conditions"
diff --git a/app/plugins/TermsAgreer/src/Controller/AgreementCollectorsController.php b/app/plugins/TermsAgreer/src/Controller/AgreementCollectorsController.php
new file mode 100644
index 000000000..80f4cd977
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/Controller/AgreementCollectorsController.php
@@ -0,0 +1,163 @@
+ [
+ 'AgreementCollectors.id' => 'asc'
+ ]
+ ];
+
+ /**
+ * Dispatch an Enrollment Flow Step.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param string $id Approval Collector ID
+ */
+
+ public function dispatch(string $id) {
+ $request = $this->getRequest();
+ $petition = $this->getPetition();
+ $coId = $this->getCOID();
+
+ // Pull the set of T&C as well as the plugin step configuration.
+ // Explicit vs Implicit is handled by the frontend.
+
+ $cfg = $this->AgreementCollectors->get($id);
+
+ if($cfg->t_and_c_mode == TAndCEnrollmentModeEnum::Ignore) {
+ // If the Plugin is set to Ignore, we simply skip this step and move on.
+
+ return $this->finishStep(
+ enrollmentFlowStepId: $cfg->enrollment_flow_step_id,
+ petitionId: $petition->id,
+ comment: __d('terms_agreer', 'result.AgreementCollectors.ignored')
+ );
+ }
+
+ // We pull Active T&C only, We pull all T&C that are not COU specific,
+ // and if there is a COU associated with this Petition then we also pull T&C
+ // for that COU.
+
+ $TermsAndConditions = TableRegistry::getTableLocator()->get('TermsAndConditions');
+
+ $whereClause = [
+ 'TermsAndConditions.co_id' => $coId,
+ 'TermsAndConditions.status' => SuspendableStatusEnum::Active
+ ];
+
+ if(!empty($petition->cou_id)) {
+ $whereClause['OR'] = [
+ 'cou_id IS NULL',
+ 'cou_id' => $petition->cou_id
+ ];
+ } else {
+ $whereClause[] = 'cou_id IS NULL';
+ }
+
+ $tandc = $TermsAndConditions->find()
+ ->contain(['MostlyStaticPages'])
+ ->where($whereClause)
+ ->order('ordr ASC')
+ ->all();
+
+ // Calculating status of current Agreements is a bit tricky, because we need to look
+ // in both the Petition (PetitionAgreements) and if this is a Petition for a Person
+ // that already exists in TAndCAgreements. As a first pass, we simply ignore any
+ // existing Agreements (eg if this is an Additional Role Enrollment) and we always
+ // require this Step to be completed. It's valid to have multiple T&C Agreements,
+ // and even desirable if they expired.
+
+ if($request->is('post')) {
+ // Walk the set of $tandc and look for an agreement in the POST data.
+ // We shouldn't really get here without all $tandc agreed to since the
+ // frontend shouldn't allow the enrollee to get here otherwise.
+
+ $data = $request->getData();
+
+ $ok = true;
+
+ foreach($tandc as $tc) {
+ // The post data is keyed on the string "tc" appended to the T&C id,
+ // and the expected value is "1".
+
+ $key = "tc".$tc->id;
+
+ if(!isset($data[$key]) || $data[$key] != "1") {
+ $ok = false;
+
+ $this->Flash->error(__d('terms_agreer','error.TAndCAgreement.missing', [$tc->description, $tc->id]));
+ }
+ }
+
+ if($ok) {
+ // Record the agreements and update the Petition
+
+ // Similar to v4, we use the Petition Token (formerly the Enrollee Token)
+ // for unauthenticated Enrollments.
+
+ $authIdentifier = $request->getSession()->read('Auth.external.user');
+
+ if(empty($authIdentifier)) {
+ $authIdentifier = $petition->token;
+ }
+
+ $this->AgreementCollectors->record(
+ petitionId: $petition->id,
+ agreementCollectorId: $cfg->id,
+ identifier: $authIdentifier,
+ tAndC: $tandc
+ );
+
+ // Redirect to the next step
+
+ return $this->finishStep(
+ enrollmentFlowStepId: $cfg->enrollment_flow_step_id,
+ petitionId: $petition->id,
+ comment: __d('terms_agreer', 'result.AgreementCollectors.recorded.summary', $tandc->count())
+ );
+ }
+ }
+
+ // If there are no pending T&C, redirect to the next step.
+
+ // Otherwise let the view render.
+ $this->set('vv_tandc_mode', $cfg->t_and_c_mode);
+ $this->set('vv_tandc', $tandc);
+
+ $this->render('/Standard/dispatch');
+ }
+}
\ No newline at end of file
diff --git a/app/plugins/TermsAgreer/src/Controller/AppController.php b/app/plugins/TermsAgreer/src/Controller/AppController.php
new file mode 100644
index 000000000..deeefd08f
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/Controller/AppController.php
@@ -0,0 +1,10 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/TermsAgreer/src/Model/Entity/PetitionAgreement.php b/app/plugins/TermsAgreer/src/Model/Entity/PetitionAgreement.php
new file mode 100644
index 000000000..6bdb3dbf7
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/Model/Entity/PetitionAgreement.php
@@ -0,0 +1,51 @@
+
+ */
+ protected array $_accessible = [
+ '*' => true,
+ 'id' => false,
+ 'slug' => false,
+ ];
+}
diff --git a/app/plugins/TermsAgreer/src/Model/Table/AgreementCollectorsTable.php b/app/plugins/TermsAgreer/src/Model/Table/AgreementCollectorsTable.php
new file mode 100644
index 000000000..41dc1699b
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/Model/Table/AgreementCollectorsTable.php
@@ -0,0 +1,237 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(TableTypeEnum::Configuration);
+
+ // Define associations
+ $this->belongsTo('EnrollmentFlowSteps');
+
+ $this->hasMany('TermsAgreer.PetitionAgreements')
+ ->setDependent(true)
+ ->setCascadeCallbacks(true);
+
+ $this->setDisplayField('id');
+
+ $this->setPrimaryLink('enrollment_flow_step_id');
+ $this->setRequiresCO(true);
+ $this->setAllowLookupPrimaryLink(['dispatch', 'display']);
+
+ $this->setAutoViewVars([
+ 'tAndCModes' => [
+ 'type' => 'enum',
+ 'class' => 'TermsAgreer.TAndCEnrollmentModeEnum'
+ ]
+ ]);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false, // Delete the pluggable object instead
+ 'dispatch' => true,
+ 'display' => true,
+ 'edit' => ['platformAdmin', 'coAdmin'],
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false, // This is added by the parent model
+ 'index' => ['platformAdmin', 'coAdmin']
+ ]
+ ]);
+ }
+
+ /**
+ * Perform steps necessary to hydrate the Person record as part of Petition finalization.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param int $id Approval Collector ID
+ * @param Petition $petition Petition
+ * @return bool true on success
+ */
+
+ public function hydrate(int $id, \App\Model\Entity\Petition $petition) {
+ // We don't currently need the configuration for anything
+ // $cfg = $this->get($id);
+
+ if(empty($petition->enrollee_person_id)) {
+ throw new \InvalidArgumentException(__d('error', 'Petitions.enrollee.notfound', [$petition->id]));
+ }
+
+ $TAndCAgreements = TableRegistry::getTableLocator()->get('TAndCAgreements');
+
+ // Retrieve the Petition Agreements and convert each one to a T&C Agreement.
+ // Fon certain types of Enrollments, it's possible that the Enrollee has already
+ // agreed to one or more T&C, but that's OK, we can simply add a new Agreement
+ // that will supercede the previous one.
+
+ $agreements = $this->PetitionAgreements
+ ->find()
+ ->where([
+ 'petition_id' => $petition->id,
+ // Strictly speaking since T&C are an all-or-nothing thing in the
+ // current implementation, we don't really care about the
+ // agreement_collector_id, but we'll filter for it anyway to
+ // maintain consistency
+ 'agreement_collector_id' => $id
+ ])
+ ->all();
+
+ foreach($agreements as $pa) {
+ $TAndCAgreements->record(
+ termsAndConditionsId: $pa->terms_and_conditions_id,
+ personId: $petition->enrollee_person_id,
+ actorPersonId: $petition->enrollee_person_id,
+ identifier: $pa->identifier,
+ agreementTime: $pa->agreement_time->getTimestamp(),
+ petitionId: $petition->id
+ );
+ }
+
+ // We recorded Petition History when the T&C were processed, and TAndCAgreements
+ // will record History above, so we don't really need to record any more here.
+
+ return true;
+ }
+
+ /**
+ * Record T&C Agreements.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param int $petitionId Petition ID
+ * @param int $agreementCollectorId Agreement Collector ID
+ * @param string $identifier Authenticated Identifier of Agreer
+ * @param ResultSet $tAndC Set of TermsAndConditions that were agreed to
+ * @throws \InvalidArgumentException
+ */
+
+ public function record(
+ int $petitionId,
+ int $agreementCollectorId,
+ string $identifier,
+ ResultSet $tAndC
+ ) {
+ $cfg = $this->get($agreementCollectorId);
+
+ $Petitions = TableRegistry::getTableLocator()->get('Petitions');
+
+ $petition = $Petitions->get($petitionId);
+
+ // Walk the set of $tAndC, upserting PetitionAgreements for each
+
+ foreach($tAndC as $tc) {
+ // Store the PetitionAgreement
+
+ $this->PetitionAgreements->upsertOrFail(
+ data: [
+ 'petition_id' => $petitionId,
+ 'agreement_collector_id' => $agreementCollectorId,
+ 'terms_and_conditions_id' => $tc->id,
+ 'identifier' => $identifier,
+ // We could probably use changelog timestamps, but it's clearer to record
+ // an explicit agreement_time. (We'd have to use modified instead of created
+ // in case we upsert, but then other changes could theoretically update the
+ // modified timestamp).
+ 'agreement_time' => date('Y-m-d H:i:s', time())
+ ],
+ whereClause: [
+ 'petition_id' => $petitionId,
+ 'agreement_collector_id' => $agreementCollectorId,
+ 'terms_and_conditions_id' => $tc->id
+ ]
+ );
+
+ // Record PetitionHistory
+
+ $Petitions->PetitionHistoryRecords->record(
+ petitionId: $petitionId,
+ enrollmentFlowStepId: $cfg->enrollment_flow_step_id,
+ action: $cfg->t_and_c_mode == TAndCEnrollmentModeEnum::ExplicitConsent ? PetitionActionEnum::TCExplicitAgreement : PetitionActionEnum::TCImpliedAgreement,
+ comment: __d('terms_agreer', 'result.AgreementCollectors.recorded', [$tc->description, $tc->id, $cfg->t_and_c_mode])
+ );
+ }
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('enrollment_flow_step_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('enrollment_flow_step_id');
+
+ $validator->add('t_and_c_mode', [
+ 'content' => ['rule' => ['inList', TAndCEnrollmentModeEnum::getConstValues()]]
+ ]);
+ $validator->notEmptyString('t_and_c_mode');
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/TermsAgreer/src/Model/Table/PetitionAgreementsTable.php b/app/plugins/TermsAgreer/src/Model/Table/PetitionAgreementsTable.php
new file mode 100644
index 000000000..2599286b0
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/Model/Table/PetitionAgreementsTable.php
@@ -0,0 +1,133 @@
+addBehavior('Changelog');
+ $this->addBehavior('Log');
+ $this->addBehavior('Timestamp');
+
+ $this->setTableType(\App\Lib\Enum\TableTypeEnum::Artifact);
+
+ // Define associations
+ $this->belongsTo('TermsAgreer.AgreementCollectors');
+ $this->belongsTo('Petitions');
+ $this->belongsTo('TermsAndConditions')
+ // It's unclear why, but Cake isn't inflecting the property key correctly here
+ // even though it does elsewhere (maybe something related to this being a plugin?)
+ ->setProperty('terms_and_conditions')
+ ->setForeignKey('terms_and_conditions_id');
+
+ $this->setDisplayField('agreement_time');
+
+ $this->setPrimaryLink('petition_id');
+ $this->setRequiresCO(true);
+
+ $this->setPermissions([
+ // Actions that operate over an entity (ie: require an $id)
+ 'entity' => [
+ 'delete' => false,
+ 'edit' => false,
+ 'view' => ['platformAdmin', 'coAdmin']
+ ],
+ // Actions that operate over a table (ie: do not require an $id)
+ 'table' => [
+ 'add' => false,
+ 'index' => false
+ ]
+ ]);
+ }
+
+ /**
+ * Set validation rules.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param Validator $validator Validator
+ * @return Validator Validator
+ */
+
+ public function validationDefault(Validator $validator): Validator {
+ $schema = $this->getSchema();
+
+ $validator->add('petition_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('petition_id');
+
+ // Strictly speaking we don't require agreement_collector_id because we always
+ // collect all T&C, at least in the current implementation. We use it partly
+ // for consistency with the the CoreEnroller plugins, and partly for future
+ // proofing (in case we eg support collecting different T&C at different times).
+ $validator->add('agreement_collector_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('agreement_collector_id');
+
+ $validator->add('terms_and_conditions_id', [
+ 'content' => ['rule' => 'isInteger']
+ ]);
+ $validator->notEmptyString('terms_and_conditions_id');
+
+ $this->registerStringValidation($validator, $schema, 'identifier', true);
+
+ $validator->add('agreement_time', [
+ 'content' => ['rule' => 'dateTime']
+ ]);
+ $validator->notEmptyString('agreement_time');
+
+ return $validator;
+ }
+}
diff --git a/app/plugins/TermsAgreer/src/TermsAgreerPlugin.php b/app/plugins/TermsAgreer/src/TermsAgreerPlugin.php
new file mode 100644
index 000000000..6d971b990
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/TermsAgreerPlugin.php
@@ -0,0 +1,98 @@
+plugin(
+ 'TermsAgreer',
+ ['path' => '/terms-agreer'],
+ function (RouteBuilder $builder) {
+ // Add custom routes here
+
+ $builder->fallbacks();
+ }
+ );
+ parent::routes($routes);
+ }
+
+ /**
+ * Add middleware for the plugin.
+ *
+ * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to update.
+ * @return \Cake\Http\MiddlewareQueue
+ */
+ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
+ {
+ // Add your middlewares here
+ // remove this method hook if you don't need it
+
+ return $middlewareQueue;
+ }
+
+ /**
+ * Add commands for the plugin.
+ *
+ * @param \Cake\Console\CommandCollection $commands The command collection to update.
+ * @return \Cake\Console\CommandCollection
+ */
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ // Add your commands here
+ // remove this method hook if you don't need it
+
+ $commands = parent::console($commands);
+
+ return $commands;
+ }
+
+ /**
+ * Register application container services.
+ *
+ * @param \Cake\Core\ContainerInterface $container The Container to update.
+ * @return void
+ * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection
+ */
+ public function services(ContainerInterface $container): void
+ {
+ // Add your services here
+ // remove this method hook if you don't need it
+ }
+}
diff --git a/app/plugins/TermsAgreer/src/View/Cell/AgreementCollectorsCell.php b/app/plugins/TermsAgreer/src/View/Cell/AgreementCollectorsCell.php
new file mode 100644
index 000000000..8e9d1a52e
--- /dev/null
+++ b/app/plugins/TermsAgreer/src/View/Cell/AgreementCollectorsCell.php
@@ -0,0 +1,96 @@
+
+ */
+ protected array $_validCellOptions = [
+ 'vv_obj',
+ 'vv_step',
+ 'viewVars',
+ ];
+
+ /**
+ * Initialization logic run at the end of object construction.
+ *
+ * @return void
+ */
+ public function initialize(): void
+ {
+ }
+
+ /**
+ * Default display method.
+ *
+ * @since COmanage Registry v5.2.0
+ * @param int $petitionId
+ * @return void
+ */
+
+ public function display(int $petitionId): void {
+ $vv_pa = $this->fetchTable('TermsAgreer.PetitionAgreements')
+ ->find()
+ ->where([
+ 'agreement_collector_id' => $this->vv_step->agreement_collector->id,
+ 'petition_id' => $this->vv_obj->id
+ ])
+ ->contain('TermsAndConditions')
+ ->all();
+
+ $this->set('vv_pa', $vv_pa);
+ }
+}
diff --git a/app/plugins/TermsAgreer/templates/AgreementCollectors/dispatch.inc b/app/plugins/TermsAgreer/templates/AgreementCollectors/dispatch.inc
new file mode 100644
index 000000000..159fe00ee
--- /dev/null
+++ b/app/plugins/TermsAgreer/templates/AgreementCollectors/dispatch.inc
@@ -0,0 +1,193 @@
+element('flash', []);
+
+// Make the Form fields editable
+$this->Field->enableFormEditMode();
+?>
+
+= __d('terms_agreer', 'information.AgreementCollectors.review') ?>
+
+Form->create(null, [
+ 'id' => 'agreement-form',
+ 'type' => 'post'
+]);
+
+
+?>
+
+
+
+ = __d('controller','TermsAndConditions',[99]) ?>
+ = __d('operation','review') ?>
+ = __d('operation','agreement') ?>
+
+
+
+
+
+ = $tc['description'] ?>
+
+
+
+ = __d('operation','review') ?>
+
+ url)): // We have a URL based T&C ?>
+
+ = __d('operation','review') . ' tc' . $tc['id'] ?>
+
+
+ = $this->element('TermsAgreer.agreeDialog', ['vv_tc' => $tc]); ?>
+
+
+
+
+ =
+ $this->Form->checkbox(
+ 'tc'.$tc['id'],
+ ['id' => 'tc'.$tc['id'], 'class' => 'form-check-input tc-agree-checkbox']
+ ) .
+ $this->Form->label(
+ 'tc'.$tc['id'],
+ __d('operation','agree'),
+ ['class' => 'form-check-label']
+ )
+ ?>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/plugins/TermsAgreer/templates/AgreementCollectors/fields.inc b/app/plugins/TermsAgreer/templates/AgreementCollectors/fields.inc
new file mode 100644
index 000000000..ae160b7cb
--- /dev/null
+++ b/app/plugins/TermsAgreer/templates/AgreementCollectors/fields.inc
@@ -0,0 +1,38 @@
+ ['EnrollmentFlowSteps', 'TermsAgreer.AgreementCollectors'],
+ 'action' => [
+ 'EnrollmentFlowSteps' => ['edit', 'view'],
+ 'TermsAgreer.AgreementCollectors' => ['edit']
+ ],
+];
diff --git a/app/plugins/TermsAgreer/templates/cell/AgreementCollectors/display.php b/app/plugins/TermsAgreer/templates/cell/AgreementCollectors/display.php
new file mode 100644
index 000000000..8d8ecefeb
--- /dev/null
+++ b/app/plugins/TermsAgreer/templates/cell/AgreementCollectors/display.php
@@ -0,0 +1,45 @@
+
+
+
+
+
+ = __d('terms_agreer', 'result.AgreementCollectors.recorded', [
+ $pa->terms_and_conditions->description,
+ $pa->terms_and_conditions->id,
+ $pa->modified
+ ]); ?>
+
+
+
+
\ No newline at end of file
diff --git a/app/plugins/TermsAgreer/templates/element/agreeDialog.php b/app/plugins/TermsAgreer/templates/element/agreeDialog.php
new file mode 100644
index 000000000..0d710126d
--- /dev/null
+++ b/app/plugins/TermsAgreer/templates/element/agreeDialog.php
@@ -0,0 +1,76 @@
+ and
+
+
+
+ = $retrySeconds ?> sec
+
+
+ = __d('information', 'user.block.message') ?>
+
+
+ = __d('information', 'user.block.retry', [$retrySeconds]) ?>
+
+ = __d('information', 'user.block.attempt', [$vv_attempts_count]) ?>
+
+
+
+
+
diff --git a/app/templates/element/pagination.php b/app/templates/element/pagination.php
index edc3cda49..5fa1be347 100644
--- a/app/templates/element/pagination.php
+++ b/app/templates/element/pagination.php
@@ -36,94 +36,100 @@
$appStateId = $this->ApplicationState->getId(ApplicationStateEnum::PaginationLimit);
?>
\ No newline at end of file
diff --git a/app/templates/element/peopleAutocomplete.php b/app/templates/element/peopleAutocomplete.php
index c545e7b1f..7df52c7f4 100644
--- a/app/templates/element/peopleAutocomplete.php
+++ b/app/templates/element/peopleAutocomplete.php
@@ -30,129 +30,166 @@
// - 'search', used when we find a person and display the fullname along with the ID
// - 'field', used for model records. It has a postfix with a link to the person canvas
$type = $type ?? 'stand-alone';
- // In the context of a type=field we will pass vv_field_arguments
- // In the context of a stand-alone field we will have vv_autocomplete_arguments
- $vv_field_arguments = $vv_field_arguments ?? $vv_autocomplete_arguments ?? [];
- $label = $label ?? $vv_field_arguments["fieldLabel"] ?? __d('operation','autocomplete.people.label');
- $fieldName = $fieldName ?? 'person_id';
- // Used by the SearchFilter Configuration
- $personType = $personType ?? 'person';
- $htmlId = $htmlId ?? 'cmPersonPickerId';
- // Does it have a value already. Default or stored
- // CAKEPHP automatically generates a select element if the value is an integer. This is not helpful here.
- $inputValue = $inputValue ?? $vv_field_arguments["fieldOptions"]["default"] ?? $vv_field_arguments["fieldOptions"]["value"] ?? '';
- // Mainly required for the Group Members people picker since this is placed as an action url
- $actionUrl = $actionUrl ?? []; // the url of the page to launch on select for a stand-alone picker
- $viewConfigParameters = $viewConfigParameters ?? [];
- $containerClasses = $containerClasses ?? 'cm-autocomplete-container';
+ // For a frozen field, print the referenced person (if one exists) and link to the person canvas.
+ if($type == 'field' && isset($vv_obj) && $vv_obj['frozen']) {
+ $personId = $vv_obj[$vv_field_arguments['fieldName']];
+ if(!empty($personId)) {
+ $personRecord = $this->Petition->getRecordForId(
+ 'person_id', $personId, ['PrimaryName', 'EmailAddresses']
+ );
+ print $this->Html->link(
+ $personRecord['primary_name']['full_name'],
+ ['controller' => 'people', 'action' => 'edit', $personId]
+ );
+ }
+ // Otherwise, build the people picker.
+ } else {
+ // In the context of a type=field we will pass vv_field_arguments
+ // In the context of a stand-alone field we will have vv_autocomplete_arguments
+ $vv_field_arguments = $vv_field_arguments ?? $vv_autocomplete_arguments ?? [];
+ $label = $label ?? $vv_field_arguments["fieldLabel"] ?? __d('operation', 'autocomplete.people.label');
+ $fieldName = $fieldName ?? 'person_id';
+ // Used by the SearchFilter Configuration
+ $personType = $personType ?? 'person';
+ $htmlId = $htmlId ?? 'person-id-picker';
+ // Does it have a value already. Default or stored
+ // CAKEPHP automatically generates a select element if the value is an integer. This is not helpful here.
+ $inputValue = $inputValue ?? $vv_field_arguments["fieldOptions"]["default"] ?? $vv_field_arguments["fieldOptions"]["value"] ?? '';
- // Load my helper functions
- $vueHelper = $this->loadHelper('Vue');
-
- // If we have the $actionUrl array, construct the URL
- $constructedActionUrl = '';
- if(!empty($actionUrl)) {
- $constructedActionUrl = $this->Url->build($actionUrl);
- }
+ // Mainly required for the Group Members people picker since this is placed as an action url
+ $actionUrl = $actionUrl ?? []; // the url of the page to launch on select for a stand-alone picker
+ $viewConfigParameters = $viewConfigParameters ?? [];
+ $containerClasses = $containerClasses ?? 'cm-autocomplete-container';
- // This is the peopleAutocomplete element. If we have the id we need to self construct the
- // - the person canvas link
- // - Get the person record for view or edit
- if (!empty($inputValue)) {
- $personRecord = $this->Petition->getRecordForId('person_id', $inputValue, ['PrimaryName', 'EmailAddresses']);
- $canvasUrl = $this->Url->build(['controller' => 'people', 'action' => 'edit', $inputValue]);
- }
-?>
+ // Load my helper functions
+ $vueHelper = $this->loadHelper('Vue');
-
+ // Mount the component and provide a global reference for this app instance.
+ window.= str_replace('-', '', $htmlId) ?> = app.mount("#= $htmlId ?>-container");
+
-
+
+
+
diff --git a/app/templates/element/searchGlobal.php b/app/templates/element/searchGlobal.php
index 4ff0ed1f4..e6fb5b364 100644
--- a/app/templates/element/searchGlobal.php
+++ b/app/templates/element/searchGlobal.php
@@ -42,7 +42,7 @@
= __d('operation','search') ?>
-
-
-
- = $this->element('flash', $vv_subnavigation_flashArgs ?? []) ?>
-
+
= $this->element('subnavigation/tabList')?>
@@ -69,9 +67,10 @@
// Do we have a nested element configured?
!empty($subNavAttributes['nested'])
// Check the nested elements if they allow navigation for this action
- && in_array($vv_action, $subNavAttributes['nested']['action'][$fullModelsName], true)
+ && in_array($vv_action, $subNavAttributes['nested']['action'][$fullModelsName] ?? [], true)
): ?>
-
+
= $this->element('subnavigation/inlineList')?>
diff --git a/app/templates/element/subnavigation/supertitle.php b/app/templates/element/subnavigation/supertitle.php
index c7354fa47..f08d75591 100644
--- a/app/templates/element/subnavigation/supertitle.php
+++ b/app/templates/element/subnavigation/supertitle.php
@@ -62,9 +62,9 @@
if (
(!empty($vv_obj) || !empty($$objectName))
&& !empty($this->getPlugin())
- && $vv_subnavigation_tabs[0] !== StringUtilities::entityToClassName($vv_bc_parent_obj)
+ && $vv_subnavigation_tabs[0] !== $vv_bc_parent_obj->getSource()
) {
- $object = $vv_obj ?? $$objectName?->first();
+ $object = $vv_obj ?? $$objectName?->items()?->first();
if ($object === null) {
// This is a deep nested association that has not been initialized yet. The controller name
// will become the supertitle
@@ -73,7 +73,7 @@
// If we get here, it means that neither the request object nor its parent can give us a supertitle.
// We need to fetch all the ids and get the supertitle from the root tab/node
$results = [];
- TableUtilities::treeTraversalFromId(StringUtilities::entityToClassName($object), (int)$object->id, $results);
+ TableUtilities::treeTraversalFromId($object->getSource(), (int)$object->id, $results);
$superTitleModelReference = $this->Tab->getModelTableReference($vv_subnavigation_tabs[0]);
$superTitleModelDisplayField = $superTitleModelReference->getDisplayField();
$superTitleModelId = $results[$vv_subnavigation_tabs[0]];
diff --git a/app/templates/element/subnavigation/tabList.php b/app/templates/element/subnavigation/tabList.php
index 58e1791db..0bc9c8023 100644
--- a/app/templates/element/subnavigation/tabList.php
+++ b/app/templates/element/subnavigation/tabList.php
@@ -32,11 +32,16 @@
$isNested = false;
extract($vv_sub_nav_attributes, EXTR_PREFIX_ALL, 'vv_subnavigation');
+$nestings = $vv_subnavigation_nested['tabs'] ?? [];
?>
Tab->getCurrentId($tab, $isNested);
// Check if there are child models. If not continue
@@ -57,14 +62,13 @@
element('subnavigation/tabTitle', compact('tab', 'curId', 'isNested'));
+ $title = $this->element('subnavigation/tabTitle', compact('tab', 'curId', 'isNested', 'tabLabel'));
// Construct Target URL
$url = $this->Tab->constructLinkUrl($tab, $curId, $isNested);
// Calculate Tab Style Class(es)
- $linkClass = $this->Tab->getLinkClass($tab, $isNested);
+ $linkClass = $this->Tab->getLinkClass($tab, $isNested, $nestings);
// Import element in the DOM
print $this->Html->link($title, $url, ['class' => $linkClass, 'escape' => false]);
?>
-
diff --git a/app/templates/element/subnavigation/tabTitle.php b/app/templates/element/subnavigation/tabTitle.php
index 9c8c6fd16..141d7f279 100644
--- a/app/templates/element/subnavigation/tabTitle.php
+++ b/app/templates/element/subnavigation/tabTitle.php
@@ -36,6 +36,7 @@
declare(strict_types = 1);
+use App\Lib\Util\StringUtilities;
use Cake\Utility\Inflector;
extract($vv_sub_nav_attributes, EXTR_PREFIX_ALL, 'vv_subnavigation');
@@ -49,51 +50,89 @@
// We calculate this first because we want to initialize the Helper variables
$tabAction = $this->Tab->getTabAction($tab, $isNested);
-$linkFilter = $this->Tab->getLinkFilter($tab, $curId, $tabAction, $isNested);
// Simple use case
$tabLanguageKey = in_array('index', $navigation_action[$tab], true) ? $tab : 'Properties';
-$title = __d('controller', $tabLanguageKey, [99]);
-$tabToTableName = Inflector::tableize(Inflector::singularize($tab));
+$title = !empty($tabLabel) ? $tabLabel : __d('controller', $tabLanguageKey, [99]);
+$request = $this->getRequest();
+$requesterModel = StringUtilities::getQualifiedName(
+ $request->getParam('plugin'),
+ $request->getParam('controller')
+);
+
+// Some tabs (eg GroupMembers from ExternalIdentityRoles context) are not directly
+// associated with the current requester model. Title rendering must not fatal.
+try {
+$tabToTableName = StringUtilities::modelNameToQualifiedModelName($tab, $requesterModel);
+} catch (\Throwable $e) {
+ $tabToTableName = $tab;
+}
// Plugin Configuration Tab
-if (str_contains($tab, '.') && in_array('edit', $navigation_action[$tab], true)) {
- $title = 'Configure Plugin';
+if (
+ str_contains($tab, '.')
+ && in_array('edit', $navigation_action[$tab], true)
+ && array_search($tab, $vv_subnavigation_tabs, true) !== 0
+) {
+ $title = __d('operation', 'configure.plugin');
} else if (str_contains($tab, '@action.')) { // Top Links/Actions
[$modelName, ] = explode('@', $tab);
[, $action] = explode('.', $tab);
$title = __d('operation', $modelName . '.' . $action);
- $tabToTableName = Inflector::tableize(Inflector::singularize($modelName));
+
+ try {
+ $tabToTableName = StringUtilities::modelNameToQualifiedModelName($modelName, $requesterModel);
+ } catch (\Throwable $e) {
+ $tabToTableName = $modelName;
+ }
} else if (str_ends_with($tab, '.Hierarchy')) { // Deep Associations
$fullModelName = $this->Tab->getAssociation();
[$plugin, $modelName] = explode('.', $fullModelName);
$poFile = Inflector::underscore($plugin);
$title = __d($poFile, 'controller.' . $modelName, [99]);
- $tabToTableName = Inflector::tableize(Inflector::singularize($fullModelName));
-} else if(str_contains($tabLanguageKey, '.')) { // Simple Plugin Plugin.Model
+
+ try {
+ $tabToTableName = StringUtilities::modelNameToQualifiedModelName($fullModelName, $requesterModel);
+ } catch (\Throwable $e) {
+ $tabToTableName = $fullModelName;
+ }
+} else if (str_contains($tabLanguageKey, '.')) { // Simple Plugin Plugin.Model
[$plugin, $modelName] = explode('.', $tabLanguageKey);
$poFile = Inflector::underscore($plugin);
$title = __d($poFile, 'controller.' . $modelName, [99]);
- $tabToTableName = Inflector::tableize(Inflector::singularize($tabLanguageKey));
+
+ try {
+ $tabToTableName = StringUtilities::modelNameToQualifiedModelName($tabLanguageKey, $requesterModel);
+ } catch (\Throwable $e) {
+ $tabToTableName = $tabLanguageKey;
+ }
}
// Insert Counter Badge if applicable
-if(isset($tab_counter)
+if (isset($tab_counter)
&& in_array($tab, $tab_counter, true)
) {
- $model = $tabToTableName;
- $where = $linkFilter;
-}
-
-if(!isset($num)
- && !empty($model)
- && !empty($where)) {
+ $url = $this->Tab->constructLinkUrl($tab, $curId, $isNested);
+ $model = $url['controller'];
+ if (!empty($url['plugin'])) {
+ $model = $url['plugin'] . '.' . $model;
+ }
+ if (isset($url['?'])) {
+ $where = $url['?'];
+ } else {
+ $passed = array_values(array_filter(
+ $url,
+ static fn($k) => is_int($k),
+ ARRAY_FILTER_USE_KEY
+ ));
+ $where = ['id' => (int)$passed];
+ }
$num = $this->Tab->getModelTotalCount($model, $where);
}
?>
-
+
= $num ?>
diff --git a/app/templates/layout/default.php b/app/templates/layout/default.php
index 2f25fc480..545eee649 100644
--- a/app/templates/layout/default.php
+++ b/app/templates/layout/default.php
@@ -72,7 +72,9 @@
print $this->Html->scriptBlock(
sprintf(
'var csrfToken = %s;',
- json_encode($this->request->getAttribute('csrfToken'), JSON_THROW_ON_ERROR)
+ json_encode(
+ $this->request->getAttribute('csrfToken') ?? $this->request->getCookie('csrfToken'),
+ JSON_THROW_ON_ERROR)
)
);
} catch (JsonException $e) {
@@ -116,7 +118,7 @@
// add hints that we're in the platform-level (COmanage) CO
$isPlatformCO = false;
- if(!empty($vv_cur_co) && ($vv_cur_co->id == 1) && !($vv_controller == 'Cos' && $vv_action == 'select')) {
+ if(!empty($vv_cur_co) && ($vv_cur_co->isCOmanageCO()) && !($vv_controller == 'Cos' && $vv_action == 'select')) {
$isPlatformCO = true;
$bodyClasses .= ' platform-co';
}
@@ -182,7 +184,7 @@
-
+
menu
@@ -215,7 +217,7 @@
= $this->element('menuMain') ?>
-
+
@@ -240,7 +242,7 @@
-
+
= $this->element('dialog') // used for confirmations ?>
diff --git a/app/templates/layout/iframe.php b/app/templates/layout/iframe.php
index 439efcc1c..c9d13e9b1 100644
--- a/app/templates/layout/iframe.php
+++ b/app/templates/layout/iframe.php
@@ -114,7 +114,7 @@
-
+
= $this->fetch('content') ?>
@@ -123,7 +123,7 @@
-
+
= $this->element('dialog') ?>
@@ -154,22 +154,24 @@
diff --git a/app/vendor/autoload.php b/app/vendor/autoload.php
index 3b4acb469..8f79a47f2 100644
--- a/app/vendor/autoload.php
+++ b/app/vendor/autoload.php
@@ -14,10 +14,7 @@
echo $err;
}
}
- trigger_error(
- $err,
- E_USER_ERROR
- );
+ throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
diff --git a/app/vendor/bin/composer b/app/vendor/bin/composer
index b8ca913e6..fd55d73eb 100755
--- a/app/vendor/bin/composer
+++ b/app/vendor/bin/composer
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/composer/composer/bin/composer');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/composer/composer/bin/composer');
}
}
-include __DIR__ . '/..'.'/composer/composer/bin/composer';
+return include __DIR__ . '/..'.'/composer/composer/bin/composer';
diff --git a/app/vendor/bin/doctrine-dbal b/app/vendor/bin/doctrine-dbal
index e86bf8dcc..4ed6f70b4 100755
--- a/app/vendor/bin/doctrine-dbal
+++ b/app/vendor/bin/doctrine-dbal
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal');
}
}
-include __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal';
+return include __DIR__ . '/..'.'/doctrine/dbal/bin/doctrine-dbal';
diff --git a/app/vendor/bin/phinx b/app/vendor/bin/phinx
deleted file mode 100755
index 9942549c0..000000000
--- a/app/vendor/bin/phinx
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/env php
-realpath = realpath($opened_path) ?: $opened_path;
- $opened_path = $this->realpath;
- $this->handle = fopen($this->realpath, $mode);
- $this->position = 0;
-
- return (bool) $this->handle;
- }
-
- public function stream_read($count)
- {
- $data = fread($this->handle, $count);
-
- if ($this->position === 0) {
- $data = preg_replace('{^#!.*\r?\n}', '', $data);
- }
-
- $this->position += strlen($data);
-
- return $data;
- }
-
- public function stream_cast($castAs)
- {
- return $this->handle;
- }
-
- public function stream_close()
- {
- fclose($this->handle);
- }
-
- public function stream_lock($operation)
- {
- return $operation ? flock($this->handle, $operation) : true;
- }
-
- public function stream_seek($offset, $whence)
- {
- if (0 === fseek($this->handle, $offset, $whence)) {
- $this->position = ftell($this->handle);
- return true;
- }
-
- return false;
- }
-
- public function stream_tell()
- {
- return $this->position;
- }
-
- public function stream_eof()
- {
- return feof($this->handle);
- }
-
- public function stream_stat()
- {
- return array();
- }
-
- public function stream_set_option($option, $arg1, $arg2)
- {
- return true;
- }
-
- public function url_stat($path, $flags)
- {
- $path = substr($path, 17);
- if (file_exists($path)) {
- return stat($path);
- }
-
- return false;
- }
- }
- }
-
- if (
- (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
- || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
- ) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx');
- exit(0);
- }
-}
-
-include __DIR__ . '/..'.'/robmorgan/phinx/bin/phinx';
diff --git a/app/vendor/bin/php-parse b/app/vendor/bin/php-parse
index 1bd2c838c..61566e60c 100755
--- a/app/vendor/bin/php-parse
+++ b/app/vendor/bin/php-parse
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
}
}
-include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
+return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
diff --git a/app/vendor/bin/phpcbf b/app/vendor/bin/phpcbf
index 0b622008f..1c0c79c40 100755
--- a/app/vendor/bin/phpcbf
+++ b/app/vendor/bin/phpcbf
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf');
}
}
-include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf';
+return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcbf';
diff --git a/app/vendor/bin/phpcs b/app/vendor/bin/phpcs
index 9eb8455a8..04e658cf9 100755
--- a/app/vendor/bin/phpcs
+++ b/app/vendor/bin/phpcs
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs');
}
}
-include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs';
+return include __DIR__ . '/..'.'/squizlabs/php_codesniffer/bin/phpcs';
diff --git a/app/vendor/bin/phpunit b/app/vendor/bin/phpunit
index e92cddc50..b5b530a8f 100755
--- a/app/vendor/bin/phpunit
+++ b/app/vendor/bin/phpunit
@@ -115,9 +115,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
}
}
-include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
+return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
diff --git a/app/vendor/bin/psysh b/app/vendor/bin/psysh
index ea7f565c1..7b983930e 100755
--- a/app/vendor/bin/psysh
+++ b/app/vendor/bin/psysh
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
}
}
-include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
+return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';
diff --git a/app/vendor/bin/sql-formatter b/app/vendor/bin/sql-formatter
index de69b8ade..ed0a69d35 100755
--- a/app/vendor/bin/sql-formatter
+++ b/app/vendor/bin/sql-formatter
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter');
}
}
-include __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter';
+return include __DIR__ . '/..'.'/doctrine/sql-formatter/bin/sql-formatter';
diff --git a/app/vendor/bin/validate-json b/app/vendor/bin/validate-json
index d077db58b..8be90f428 100755
--- a/app/vendor/bin/validate-json
+++ b/app/vendor/bin/validate-json
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json');
}
}
-include __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json';
+return include __DIR__ . '/..'.'/justinrainbow/json-schema/bin/validate-json';
diff --git a/app/vendor/bin/var-dump-server b/app/vendor/bin/var-dump-server
index c52c77272..18db1c1eb 100755
--- a/app/vendor/bin/var-dump-server
+++ b/app/vendor/bin/var-dump-server
@@ -112,9 +112,8 @@ if (PHP_VERSION_ID < 80000) {
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
- include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
- exit(0);
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
}
}
-include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
+return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
diff --git a/app/vendor/brick/varexporter/CHANGELOG.md b/app/vendor/brick/varexporter/CHANGELOG.md
index f636ce3fa..923e9c3b0 100644
--- a/app/vendor/brick/varexporter/CHANGELOG.md
+++ b/app/vendor/brick/varexporter/CHANGELOG.md
@@ -1,5 +1,42 @@
# Changelog
+## [0.7.0](https://github.com/brick/varexporter/releases/tag/0.7.0) - 2026-01-06
+
+✨ **New features**
+
+- Added `VarExporter::INLINE_LITERAL_LIST`, similar to `INLINE_SCALAR_LIST` but includes enum values
+
+💥 **BC breaks**
+
+- Minimum PHP version is now `8.2`
+
+## [0.6.0](https://github.com/brick/varexporter/releases/tag/0.6.0) - 2025-02-20
+
+💥 **BC breaks**
+
+- Minimum PHP version is now `8.1` (#39)
+
+✨ **New features**
+
+- Support for exporting `match` constructs in closures (#38)
+
+Thanks to @reinfi!
+
+## [0.5.0](https://github.com/brick/varexporter/releases/tag/0.5.0) - 2024-05-10
+
+✨ **Compatibility**
+
+- Added compatibility with `nikic/php-parser` `5.x`
+- Removed compatibility with `nikic/php-parser` `4.x`
+
+💥 **BC breaks**
+
+- deprecated constant `VarExporter::INLINE_NUMERIC_SCALAR_ARRAY` has been removed, please use `INLINE_SCALAR_LIST` instead
+
+## [0.4.0](https://github.com/brick/varexporter/releases/tag/0.4.0) - 2023-09-01
+
+Minimum PHP version is now `7.4`. No breaking changes.
+
## [0.3.8](https://github.com/brick/varexporter/releases/tag/0.3.8) - 2023-01-22
✨ **New feature**
diff --git a/app/vendor/brick/varexporter/composer.json b/app/vendor/brick/varexporter/composer.json
index 916985d8c..682066f47 100644
--- a/app/vendor/brick/varexporter/composer.json
+++ b/app/vendor/brick/varexporter/composer.json
@@ -7,13 +7,13 @@
],
"license": "MIT",
"require": {
- "php": "^7.2 || ^8.0",
- "nikic/php-parser": "^4.0"
+ "php": "^8.2",
+ "nikic/php-parser": "^5.0"
},
"require-dev": {
- "phpunit/phpunit": "^8.5 || ^9.0",
+ "phpunit/phpunit": "^11.0",
"php-coveralls/php-coveralls": "^2.2",
- "vimeo/psalm": "4.23.0"
+ "vimeo/psalm": "6.14.3"
},
"autoload": {
"psr-4": {
diff --git a/app/vendor/brick/varexporter/src/ExportException.php b/app/vendor/brick/varexporter/src/ExportException.php
index 888cf9acd..18049a67e 100644
--- a/app/vendor/brick/varexporter/src/ExportException.php
+++ b/app/vendor/brick/varexporter/src/ExportException.php
@@ -4,14 +4,15 @@
namespace Brick\VarExporter;
+use Exception;
use Throwable;
-final class ExportException extends \Exception
+use function implode;
+
+final class ExportException extends Exception
{
/**
- * @param string $message
- * @param string[] $path
- * @param Throwable|null $previous
+ * @param string[] $path
*/
public function __construct(string $message, array $path, ?Throwable $previous = null)
{
@@ -26,10 +27,8 @@ public function __construct(string $message, array $path, ?Throwable $previous =
* Returns a string representation of the given path.
*
* @param string[] $path
- *
- * @return string
*/
- public static function pathToString(array $path) : string
+ public static function pathToString(array $path): string
{
return '[' . implode('][', $path) . ']';
}
diff --git a/app/vendor/brick/varexporter/src/Internal/GenericExporter.php b/app/vendor/brick/varexporter/src/Internal/GenericExporter.php
index 6b520a231..5f677edf6 100644
--- a/app/vendor/brick/varexporter/src/Internal/GenericExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/GenericExporter.php
@@ -6,6 +6,21 @@
use Brick\VarExporter\ExportException;
use Brick\VarExporter\VarExporter;
+use ReflectionObject;
+use UnitEnum;
+
+use function array_is_list;
+use function array_merge;
+use function array_unshift;
+use function count;
+use function gettype;
+use function implode;
+use function is_array;
+use function is_object;
+use function is_scalar;
+use function spl_object_id;
+use function sprintf;
+use function var_export;
/**
* The main exporter implementation, that handles variables of any type.
@@ -18,101 +33,73 @@
*/
final class GenericExporter
{
- /**
- * @var ObjectExporter[]
- */
- private $objectExporters = [];
+ public readonly bool $addTypeHints;
- /**
- * The visited objects, to detect circular references.
- *
- * This is a two-level map of parent object id => child object id => path where the object first appeared.
- *
- * @var array>
- */
- private $visitedObjects = [];
+ public readonly bool $skipDynamicProperties;
- /**
- * @psalm-readonly
- *
- * @var bool
- */
- public $addTypeHints;
+ public readonly bool $inlineArray;
- /**
- * @psalm-readonly
- *
- * @var bool
- */
- public $skipDynamicProperties;
+ public readonly bool $inlineScalarList;
- /**
- * @psalm-readonly
- *
- * @var bool
- */
- public $inlineArray;
+ public readonly bool $inlineLiteralList;
- /**
- * @psalm-readonly
- *
- * @var bool
- */
- public $inlineScalarList;
+ public readonly bool $closureSnapshotUses;
- /**
- * @psalm-readonly
- *
- * @var bool
- */
- public $closureSnapshotUses;
+ public readonly bool $trailingCommaInArray;
+
+ public readonly int $indentLevel;
/**
- * @psalm-readonly
- *
- * @var bool
+ * @var ObjectExporter[]
*/
- public $trailingCommaInArray;
+ private readonly array $objectExporters;
/**
- * @psalm-readonly
+ * The visited objects, to detect circular references.
*
- * @var int
+ * This is a two-level map of parent object id => child object id => path where the object first appeared.
+ *
+ * @var array>
*/
- public $indentLevel;
+ private array $visitedObjects = [];
public function __construct(int $options, int $indentLevel = 0)
{
- $this->objectExporters[] = new ObjectExporter\StdClassExporter($this);
+ $objectExporters = [
+ new ObjectExporter\StdClassExporter($this),
+ ];
- if (! ($options & VarExporter::NO_CLOSURES)) {
- $this->objectExporters[] = new ObjectExporter\ClosureExporter($this);
+ if (($options & VarExporter::NO_CLOSURES) === 0) {
+ $objectExporters[] = new ObjectExporter\ClosureExporter($this);
}
- if (! ($options & VarExporter::NO_SET_STATE)) {
- $this->objectExporters[] = new ObjectExporter\SetStateExporter($this);
+ if (($options & VarExporter::NO_SET_STATE) === 0) {
+ $objectExporters[] = new ObjectExporter\SetStateExporter($this);
}
- $this->objectExporters[] = new ObjectExporter\InternalClassExporter($this);
+ $objectExporters[] = new ObjectExporter\InternalClassExporter($this);
- if (! ($options & VarExporter::NO_SERIALIZE)) {
- $this->objectExporters[] = new ObjectExporter\SerializeExporter($this);
+ if (($options & VarExporter::NO_SERIALIZE) === 0) {
+ $objectExporters[] = new ObjectExporter\SerializeExporter($this);
}
- if (! ($options & VarExporter::NO_ENUMS)) {
- $this->objectExporters[] = new ObjectExporter\EnumExporter($this);
+ if (($options & VarExporter::NO_ENUMS) === 0) {
+ $objectExporters[] = new ObjectExporter\EnumExporter($this);
}
- if (! ($options & VarExporter::NOT_ANY_OBJECT)) {
- $this->objectExporters[] = new ObjectExporter\AnyObjectExporter($this);
+ if (($options & VarExporter::NOT_ANY_OBJECT) === 0) {
+ $objectExporters[] = new ObjectExporter\AnyObjectExporter($this);
}
- $this->addTypeHints = (bool) ($options & VarExporter::ADD_TYPE_HINTS);
- $this->skipDynamicProperties = (bool) ($options & VarExporter::SKIP_DYNAMIC_PROPERTIES);
- $this->inlineArray = (bool) ($options & VarExporter::INLINE_ARRAY);
- $this->inlineScalarList = (bool) ($options & VarExporter::INLINE_SCALAR_LIST);
- $this->closureSnapshotUses = (bool) ($options & VarExporter::CLOSURE_SNAPSHOT_USES);
- $this->trailingCommaInArray = (bool) ($options & VarExporter::TRAILING_COMMA_IN_ARRAY);
+ $this->objectExporters = $objectExporters;
+
+ $this->addTypeHints = (bool) ($options & VarExporter::ADD_TYPE_HINTS);
+ $this->skipDynamicProperties = (bool) ($options & VarExporter::SKIP_DYNAMIC_PROPERTIES);
+ $this->inlineArray = (bool) ($options & VarExporter::INLINE_ARRAY);
+ $this->inlineScalarList = (bool) ($options & VarExporter::INLINE_SCALAR_LIST);
+ $this->inlineLiteralList = (bool) ($options & VarExporter::INLINE_LITERAL_LIST);
+ $this->closureSnapshotUses = (bool) ($options & VarExporter::CLOSURE_SNAPSHOT_USES);
+ $this->trailingCommaInArray = (bool) ($options & VarExporter::TRAILING_COMMA_IN_ARRAY);
$this->indentLevel = $indentLevel;
}
@@ -126,36 +113,30 @@ public function __construct(int $options, int $indentLevel = 0)
*
* @throws ExportException
*/
- public function export($var, array $path, array $parentIds) : array
+ public function export(mixed $var, array $path, array $parentIds): array
{
- switch ($type = gettype($var)) {
- case 'boolean':
- case 'integer':
- case 'double':
- case 'string':
- return [var_export($var, true)];
-
- case 'NULL':
- // lowercase null
- return ['null'];
-
- case 'array':
- /** @var array $var */
- return $this->exportArray($var, $path, $parentIds);
-
- case 'object':
- /** @var object $var */
- return $this->exportObject($var, $path, $parentIds);
-
- default:
- // resources
- throw new ExportException(sprintf('Type "%s" is not supported.', $type), $path);
+ if ($var === null) {
+ return ['null'];
+ }
+
+ // bool, int, float, string
+ if (is_scalar($var)) {
+ return [var_export($var, true)];
}
+
+ if (is_array($var)) {
+ return $this->exportArray($var, $path, $parentIds);
+ }
+
+ if (is_object($var)) {
+ return $this->exportObject($var, $path, $parentIds);
+ }
+
+ // resources
+ throw new ExportException(sprintf('Type "%s" is not supported.', gettype($var)), $path);
}
/**
- * @psalm-suppress MixedAssignment
- *
* @param array $array The array to export.
* @param string[] $path The path to the current array in the array/object graph.
* @param int[] $parentIds The ids of all objects higher in the graph.
@@ -163,8 +144,10 @@ public function export($var, array $path, array $parentIds) : array
* @return string[] The lines of code.
*
* @throws ExportException
+ *
+ * @psalm-suppress MixedAssignment
*/
- public function exportArray(array $array, array $path, array $parentIds) : array
+ public function exportArray(array $array, array $path, array $parentIds): array
{
if (! $array) {
return ['[]'];
@@ -173,11 +156,13 @@ public function exportArray(array $array, array $path, array $parentIds) : array
$result = [];
$count = count($array);
- $isList = array_keys($array) === range(0, $count - 1);
+ $isList = array_is_list($array);
$current = 0;
- $inline = $this->inlineArray || ($this->inlineScalarList && $isList && $this->isScalarList($array));
+ $inline = $this->inlineArray
+ || ($this->inlineScalarList && $isList && $this->isScalarList($array))
+ || ($this->inlineLiteralList && $isList && $this->isLiteralList($array));
foreach ($array as $key => $value) {
$isLast = (++$current === $count);
@@ -188,11 +173,7 @@ public function exportArray(array $array, array $path, array $parentIds) : array
$exported = $this->export($value, $newPath, $parentIds);
if ($inline) {
- if ($isList) {
- $result[] = $exported[0];
- } else {
- $result[] = var_export($key, true) . ' => ' . $exported[0];
- }
+ $result[] = $isList ? $exported[0] : var_export($key, true) . ' => ' . $exported[0];
} else {
$prepend = '';
$append = '';
@@ -222,27 +203,6 @@ public function exportArray(array $array, array $path, array $parentIds) : array
return $result;
}
- /**
- * Returns whether the given array only contains scalar values.
- *
- * Types considered scalar here are int, bool, float, string and null.
- * If the array is empty, this method returns true.
- *
- * @param array $array
- *
- * @return bool
- */
- private function isScalarList(array $array) : bool
- {
- foreach ($array as $value) {
- if ($value !== null && ! is_scalar($value)) {
- return false;
- }
- }
-
- return true;
- }
-
/**
* @param object $object The object to export.
* @param string[] $path The path to the current object in the array/object graph.
@@ -252,7 +212,7 @@ private function isScalarList(array $array) : bool
*
* @throws ExportException
*/
- public function exportObject(object $object, array $path, array $parentIds) : array
+ public function exportObject(object $object, array $path, array $parentIds): array
{
$id = spl_object_id($object);
@@ -261,15 +221,15 @@ public function exportObject(object $object, array $path, array $parentIds) : ar
throw new ExportException(sprintf(
'Object of class "%s" has a circular reference at %s. ' .
'Circular references are currently not supported.',
- get_class($object),
- ExportException::pathToString($this->visitedObjects[$parentId][$id])
+ $object::class,
+ ExportException::pathToString($this->visitedObjects[$parentId][$id]),
), $path);
}
$this->visitedObjects[$parentId][$id] = $path;
}
- $reflectionObject = new \ReflectionObject($object);
+ $reflectionObject = new ReflectionObject($object);
foreach ($this->objectExporters as $objectExporter) {
if ($objectExporter->supports($reflectionObject)) {
@@ -291,9 +251,9 @@ public function exportObject(object $object, array $path, array $parentIds) : ar
*
* @return string[] The indented lines of code.
*/
- public function indent(array $lines) : array
+ public function indent(array $lines): array
{
- foreach ($lines as & $value) {
+ foreach ($lines as &$value) {
if ($value !== '') {
$value = ' ' . $value;
}
@@ -309,11 +269,45 @@ public function indent(array $lines) : array
*
* @return string[]
*/
- public function wrap(array $lines, string $prepend, string $append) : array
+ public function wrap(array $lines, string $prepend, string $append): array
{
$lines[0] = $prepend . $lines[0];
$lines[count($lines) - 1] .= $append;
return $lines;
}
+
+ /**
+ * Returns whether the given array only contains scalar values.
+ *
+ * Types considered scalar here are int, bool, float, string and null.
+ * If the array is empty, this method returns true.
+ */
+ private function isScalarList(array $array): bool
+ {
+ foreach ($array as $value) {
+ if ($value !== null && ! is_scalar($value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether the given array only contains literal values.
+ *
+ * Values considered literal are: int, float, string, bool, null, and enum values.
+ * If the array is empty, this method returns true.
+ */
+ private function isLiteralList(array $array): bool
+ {
+ foreach ($array as $value) {
+ if ($value !== null && ! is_scalar($value) && ! $value instanceof UnitEnum) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php
index 9b34614ee..02fb7c355 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter.php
@@ -5,6 +5,10 @@
namespace Brick\VarExporter\Internal;
use Brick\VarExporter\ExportException;
+use ReflectionClass;
+use ReflectionObject;
+
+use function array_merge;
/**
* An exporter that handles a specific type of object.
@@ -13,52 +17,40 @@
*/
abstract class ObjectExporter
{
- /**
- * @var GenericExporter
- */
- protected $exporter;
-
- /**
- * @param GenericExporter $exporter
- */
- public function __construct(GenericExporter $exporter)
- {
- $this->exporter = $exporter;
+ public function __construct(
+ protected readonly GenericExporter $exporter,
+ ) {
}
/**
* Returns whether this exporter supports the given object.
*
- * @param \ReflectionObject $reflectionObject A reflection of the object.
- *
- * @return bool
+ * @param ReflectionObject $reflectionObject A reflection of the object.
*/
- abstract public function supports(\ReflectionObject $reflectionObject) : bool;
+ abstract public function supports(ReflectionObject $reflectionObject): bool;
/**
* Exports the given object.
*
- * @param object $object The object to export.
- * @param \ReflectionObject $reflectionObject A reflection of the object.
- * @param string[] $path The path to the current object in the array/object graph.
- * @param int[] $parentIds The ids of all objects higher in the graph.
+ * @param object $object The object to export.
+ * @param ReflectionObject $reflectionObject A reflection of the object.
+ * @param string[] $path The path to the current object in the array/object graph.
+ * @param int[] $parentIds The ids of all objects higher in the graph.
*
* @return string[] The lines of code.
*
* @throws ExportException
*/
- abstract public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array;
+ abstract public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array;
/**
* Returns the code to create a new object of the given class.
*
* If the class has a constructor, reflection will be used to bypass it.
*
- * @param \ReflectionClass $class
- *
* @return string[] The lines of code.
*/
- final protected function getCreateObjectCode(\ReflectionClass $class) : array
+ final protected function getCreateObjectCode(ReflectionClass $class): array
{
$className = '\\' . $class->getName();
@@ -85,12 +77,12 @@ final protected function getCreateObjectCode(\ReflectionClass $class) : array
*
* @return string[] The lines of code, wrapped in a closure.
*/
- final protected function wrapInClosure(array $code) : array
+ final protected function wrapInClosure(array $code): array
{
return array_merge(
['(static function() {'],
$this->exporter->indent($code),
- ['})()']
+ ['})()'],
);
}
}
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php
index 7cb3b6292..2ab0416d1 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/AnyObjectExporter.php
@@ -5,6 +5,17 @@
namespace Brick\VarExporter\Internal\ObjectExporter;
use Brick\VarExporter\Internal\ObjectExporter;
+use Override;
+use ReflectionClass;
+use ReflectionObject;
+use ReflectionProperty;
+
+use function array_key_exists;
+use function array_merge;
+use function method_exists;
+use function preg_match;
+use function spl_object_id;
+use function var_export;
/**
* Handles any class through direct property access and bound closures.
@@ -14,30 +25,27 @@
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class AnyObjectExporter extends ObjectExporter
+final class AnyObjectExporter extends ObjectExporter
{
- /**
- * {@inheritDoc}
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
return true;
}
/**
- * {@inheritDoc}
- *
* @psalm-suppress MixedAssignment
*/
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
$lines = $this->getCreateObjectCode($reflectionObject);
$objectAsArray = (array) $object;
$current = $this->exporter->skipDynamicProperties
- ? new \ReflectionClass($object) // properties from class definition only
- : $reflectionObject; // properties from class definition + dynamic properties
+ ? new ReflectionClass($object) // properties from class definition only
+ : $reflectionObject; // properties from class definition + dynamic properties
$isParentClass = false;
@@ -70,13 +78,13 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra
if (array_key_exists($key, $objectAsArray)) {
$value = $objectAsArray[$key];
- if ($property->isPublic() && !(method_exists($property, 'isReadOnly') && $property->isReadOnly())) {
+ if ($property->isPublic() && ! (method_exists($property, 'isReadOnly') && $property->isReadOnly())) {
$publicNonReadonlyProperties[$name] = $value;
} else {
$nonPublicOrPublicReadonlyProperties[$name] = $value;
}
} else {
- if ($property->isPublic() && !(method_exists($property, 'isReadOnly') && $property->isReadOnly())) {
+ if ($property->isPublic() && ! (method_exists($property, 'isReadOnly') && $property->isReadOnly())) {
$unsetPublicNonReadonlyProperties[] = $name;
} else {
$unsetNonPublicOrPublicReadonlyProperties[] = $name;
@@ -155,12 +163,8 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra
/**
* Returns the key of the given property in the object-to-array cast.
- *
- * @param \ReflectionProperty $property
- *
- * @return string
*/
- private function getPropertyKey(\ReflectionProperty $property) : string
+ private function getPropertyKey(ReflectionProperty $property): string
{
$name = $property->getName();
@@ -175,12 +179,7 @@ private function getPropertyKey(\ReflectionProperty $property) : string
return $name;
}
- /**
- * @param string $var
- *
- * @return string
- */
- private function escapePropName(string $var) : string
+ private function escapePropName(string $var): string
{
if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $var) === 1) {
return $var;
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php
index ea98891cc..1c2ecb734 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter.php
@@ -7,6 +7,7 @@
use Brick\VarExporter\ExportException;
use Brick\VarExporter\Internal\ObjectExporter;
use Closure;
+use Override;
use PhpParser\Error;
use PhpParser\Node;
use PhpParser\NodeTraverser;
@@ -15,35 +16,41 @@
use PhpParser\Parser;
use PhpParser\ParserFactory;
use ReflectionFunction;
+use ReflectionObject;
+
+use function array_keys;
+use function array_merge;
+use function assert;
+use function count;
+use function file_get_contents;
+use function implode;
+use function is_string;
+use function sprintf;
+use function str_ends_with;
+
+use const PHP_EOL;
/**
* Handles closures.
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class ClosureExporter extends ObjectExporter
+final class ClosureExporter extends ObjectExporter
{
- /**
- * @var Parser|null
- */
- private $parser;
+ private ?Parser $parser = null;
- /**
- * {@inheritDoc}
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
- return $reflectionObject->getName() === \Closure::class;
+ return $reflectionObject->getName() === Closure::class;
}
- /**
- * {@inheritDoc}
- */
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
assert($object instanceof Closure);
- $reflectionFunction = new \ReflectionFunction($object);
+ $reflectionFunction = new ReflectionFunction($object);
$file = $reflectionFunction->getFileName();
$line = $reflectionFunction->getStartLine();
@@ -64,13 +71,10 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra
return [$code];
}
- /**
- * @return Parser
- */
- private function getParser()
+ private function getParser(): Parser
{
if ($this->parser === null) {
- $this->parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7);
+ $this->parser = (new ParserFactory())->createForHostVersion();
}
return $this->parser;
@@ -86,13 +90,13 @@ private function getParser()
*
* @throws ExportException
*/
- private function parseFile(string $filename, array $path) : array
+ private function parseFile(string $filename, array $path): array
{
- if (substr($filename, -16) === " : eval()'d code") {
+ if (str_ends_with($filename, " : eval()'d code")) {
throw new ExportException("Closure defined in eval()'d code cannot be exported.", $path);
}
- $source = @ file_get_contents($filename);
+ $source = @file_get_contents($filename);
if ($source === false) {
// @codeCoverageIgnoreStart
@@ -121,7 +125,7 @@ private function parseFile(string $filename, array $path) : array
*
* @return Node[]
*/
- private function resolveNames(array $ast) : array
+ private function resolveNames(array $ast): array
{
$nameResolver = new NameResolver();
$nodeTraverser = new NodeTraverser();
@@ -139,8 +143,6 @@ private function resolveNames(array $ast) : array
* @param int $line The line number where the closure is located in the source file.
* @param string[] $path The path to the closure in the array/object graph.
*
- * @return Node\Expr\Closure
- *
* @throws ExportException
*/
private function getClosure(
@@ -148,12 +150,12 @@ private function getClosure(
array $ast,
string $file,
int $line,
- array $path
- ) : Node\Expr\Closure {
- $finder = new FindingVisitor(function(Node $node) use ($line) : bool {
- return ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction)
- && $node->getStartLine() === $line;
- });
+ array $path,
+ ): Node\Expr\Closure {
+ $finder = new FindingVisitor(
+ fn (Node $node): bool => ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction)
+ && $node->getStartLine() === $line,
+ );
$traverser = new NodeTraverser();
$traverser->addVisitor($finder);
@@ -167,11 +169,11 @@ private function getClosure(
'Expected exactly 1 closure in %s on line %d, found %d.',
$file,
$line,
- $count
+ $count,
), $path);
}
- /** @var Node\Expr\Closure|Node\Expr\ArrowFunction $closure */
+ /** @var Node\Expr\ArrowFunction|Node\Expr\Closure $closure */
$closure = $closures[0];
if ($closure instanceof Node\Expr\ArrowFunction) {
@@ -188,15 +190,13 @@ private function getClosure(
/**
* Convert a parsed arrow function to a closure.
*
- * @param ReflectionFunction $reflectionFunction Reflection of the closure.
- * @param Node\Expr\ArrowFunction $arrowFunction Parsed arrow function.
- *
- * @return Node\Expr\Closure
+ * @param ReflectionFunction $reflectionFunction Reflection of the closure.
+ * @param Node\Expr\ArrowFunction $arrowFunction Parsed arrow function.
*/
private function convertArrowFunction(
ReflectionFunction $reflectionFunction,
- Node\Expr\ArrowFunction $arrowFunction
- ) : Node\Expr\Closure {
+ Node\Expr\ArrowFunction $arrowFunction,
+ ): Node\Expr\Closure {
$closure = new Node\Expr\Closure([], ['arrow_function' => true]);
$closure->static = false;
@@ -210,8 +210,8 @@ private function convertArrowFunction(
foreach (array_keys($static) as $var) {
assert(is_string($var));
- $closure->uses[] = new Node\Expr\ClosureUse(
- new Node\Expr\Variable($var)
+ $closure->uses[] = new Node\ClosureUse(
+ new Node\Expr\Variable($var),
);
}
@@ -230,11 +230,11 @@ private function convertArrowFunction(
private function closureHandleUses(
ReflectionFunction $reflectionFunction,
Node\Expr\Closure $closure,
- array $path
- ) : void {
+ array $path,
+ ): void {
if (! $this->exporter->closureSnapshotUses) {
$message = $closure->hasAttribute('arrow_function')
- ? "The arrow function uses variables in the parent scope, this is not supported by default"
+ ? 'The arrow function uses variables in the parent scope, this is not supported by default'
: "The closure has bound variables through 'use', this is not supported by default";
throw new ExportException("$message. Use the CLOSURE_SNAPSHOT_USE option to export them.", $path);
@@ -261,7 +261,7 @@ private function closureHandleUses(
$assign = new Node\Expr\Assign(
new Node\Expr\Variable($var),
- $expr->expr
+ $expr->expr,
);
$stmts[] = new Node\Stmt\Expression($assign);
}
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php
index 4010810a4..7b89289ba 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/ClosureExporter/PrettyPrinter.php
@@ -4,8 +4,11 @@
namespace Brick\VarExporter\Internal\ObjectExporter\ClosureExporter;
+use Override;
use PhpParser\PrettyPrinter\Standard;
+use function str_repeat;
+
/**
* Extends the standard pretty-printer to allow for a base indent level.
*
@@ -13,25 +16,15 @@
*/
final class PrettyPrinter extends Standard
{
- /**
- * @var int
- */
- private $varExporterNestingLevel = 0;
-
- /**
- * @param int $level
- *
- * @return void
- */
- public function setVarExporterNestingLevel(int $level) : void
+ private int $varExporterNestingLevel = 0;
+
+ public function setVarExporterNestingLevel(int $level): void
{
$this->varExporterNestingLevel = $level;
}
- /**
- * {@inheritDoc}
- */
- protected function resetState() : void
+ #[Override]
+ protected function resetState(): void
{
parent::resetState();
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php
index dfec28f42..77cda93e4 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/EnumExporter.php
@@ -5,35 +5,32 @@
namespace Brick\VarExporter\Internal\ObjectExporter;
use Brick\VarExporter\Internal\ObjectExporter;
+use Override;
+use ReflectionObject;
use UnitEnum;
+use function assert;
+
/**
* Handles enums.
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class EnumExporter extends ObjectExporter
+final class EnumExporter extends ObjectExporter
{
- /**
- * {@inheritDoc}
- *
- * See: https://github.com/vimeo/psalm/pull/8117
- * @psalm-suppress RedundantCondition
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
- return method_exists($reflectionObject, 'isEnum') && $reflectionObject->isEnum();
+ return $reflectionObject->isEnum();
}
- /**
- * {@inheritDoc}
- */
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
assert($object instanceof UnitEnum);
return [
- get_class($object) . '::' . $object->name
+ $object::class . '::' . $object->name,
];
}
}
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php
index e0adbd111..62be3135d 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/InternalClassExporter.php
@@ -6,26 +6,24 @@
use Brick\VarExporter\ExportException;
use Brick\VarExporter\Internal\ObjectExporter;
+use Override;
+use ReflectionObject;
/**
* Throws on internal classes.
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class InternalClassExporter extends ObjectExporter
+final class InternalClassExporter extends ObjectExporter
{
- /**
- * {@inheritDoc}
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
return $reflectionObject->isInternal();
}
- /**
- * {@inheritDoc}
- */
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
$className = $reflectionObject->getName();
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php
index 604a104ea..639ac40f9 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SerializeExporter.php
@@ -5,27 +5,27 @@
namespace Brick\VarExporter\Internal\ObjectExporter;
use Brick\VarExporter\Internal\ObjectExporter;
+use Override;
+use ReflectionObject;
+
+use function array_merge;
/**
* Handles instances of classes with __serialize() and __unserialize() methods.
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class SerializeExporter extends ObjectExporter
+final class SerializeExporter extends ObjectExporter
{
- /**
- * {@inheritDoc}
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
return $reflectionObject->hasMethod('__serialize')
&& $reflectionObject->hasMethod('__unserialize');
}
- /**
- * {@inheritDoc}
- */
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
$lines = $this->getCreateObjectCode($reflectionObject);
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php
index a464b992a..d760c2117 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/SetStateExporter.php
@@ -6,18 +6,23 @@
use Brick\VarExporter\ExportException;
use Brick\VarExporter\Internal\ObjectExporter;
+use Override;
+use ReflectionClass;
+use ReflectionObject;
+
+use function array_key_exists;
+use function strrpos;
+use function substr;
/**
* Handles instances of classes with a __set_state() method.
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class SetStateExporter extends ObjectExporter
+final class SetStateExporter extends ObjectExporter
{
- /**
- * {@inheritDoc}
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
if ($reflectionObject->hasMethod('__set_state')) {
$method = $reflectionObject->getMethod('__set_state');
@@ -28,19 +33,16 @@ public function supports(\ReflectionObject $reflectionObject) : bool
return false;
}
- /**
- * {@inheritDoc}
- */
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
$className = $reflectionObject->getName();
$vars = $this->getObjectVars($object, $path);
$exportedVars = $this->exporter->exportArray($vars, $path, $parentIds);
- $exportedVars = $this->exporter->wrap($exportedVars, '\\' . $className . '::__set_state(', ')');
- return $exportedVars;
+ return $this->exporter->wrap($exportedVars, '\\' . $className . '::__set_state(', ')');
}
/**
@@ -55,16 +57,16 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra
*
* This way we offer a better safety guarantee, while staying compatible with var_export() in the output.
*
- * @psalm-suppress MixedAssignment
- *
* @param object $object The object to dump.
* @param string[] $path The path to the object, in the array/object graph.
*
* @return array An associative array of property name to value.
*
* @throws ExportException
+ *
+ * @psalm-suppress MixedAssignment
*/
- private function getObjectVars(object $object, array $path) : array
+ private function getObjectVars(object $object, array $path): array
{
$result = [];
@@ -76,15 +78,13 @@ private function getObjectVars(object $object, array $path) : array
$name = substr($name, $pos + 1);
}
- assert($name !== false);
-
if (array_key_exists($name, $result)) {
- $className = get_class($object);
+ $className = $object::class;
throw new ExportException(
'Class "' . $className . '" has overridden private property "' . $name . '". ' .
'This is not supported for exporting objects with __set_state().',
- $path
+ $path,
);
}
@@ -98,16 +98,10 @@ private function getObjectVars(object $object, array $path) : array
return $result;
}
- /**
- * @param object $object
- * @param string $name
- *
- * @return bool
- */
- private function isDynamicProperty(object $object, string $name) : bool
+ private function isDynamicProperty(object $object, string $name): bool
{
- $reflectionClass = new \ReflectionClass($object);
- $reflectionObject = new \ReflectionObject($object);
+ $reflectionClass = new ReflectionClass($object);
+ $reflectionObject = new ReflectionObject($object);
return $reflectionObject->hasProperty($name) && ! $reflectionClass->hasProperty($name);
}
diff --git a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php
index ac958f18f..6e49dcddd 100644
--- a/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php
+++ b/app/vendor/brick/varexporter/src/Internal/ObjectExporter/StdClassExporter.php
@@ -5,26 +5,25 @@
namespace Brick\VarExporter\Internal\ObjectExporter;
use Brick\VarExporter\Internal\ObjectExporter;
+use Override;
+use ReflectionObject;
+use stdClass;
/**
* Handles stdClass objects.
*
* @internal This class is for internal use, and not part of the public API. It may change at any time without warning.
*/
-class StdClassExporter extends ObjectExporter
+final class StdClassExporter extends ObjectExporter
{
- /**
- * {@inheritDoc}
- */
- public function supports(\ReflectionObject $reflectionObject) : bool
+ #[Override]
+ public function supports(ReflectionObject $reflectionObject): bool
{
- return $reflectionObject->getName() === \stdClass::class;
+ return $reflectionObject->getName() === stdClass::class;
}
- /**
- * {@inheritDoc}
- */
- public function export(object $object, \ReflectionObject $reflectionObject, array $path, array $parentIds) : array
+ #[Override]
+ public function export(object $object, ReflectionObject $reflectionObject, array $path, array $parentIds): array
{
$exported = $this->exporter->exportArray((array) $object, $path, $parentIds);
diff --git a/app/vendor/brick/varexporter/src/VarExporter.php b/app/vendor/brick/varexporter/src/VarExporter.php
index 21d6e4fcd..687a1cae7 100644
--- a/app/vendor/brick/varexporter/src/VarExporter.php
+++ b/app/vendor/brick/varexporter/src/VarExporter.php
@@ -6,6 +6,14 @@
use Brick\VarExporter\Internal\GenericExporter;
+use function array_map;
+use function array_shift;
+use function count;
+use function implode;
+use function str_repeat;
+
+use const PHP_EOL;
+
final class VarExporter
{
/**
@@ -50,15 +58,10 @@ final class VarExporter
/**
* Formats lists (0-based numeric arrays) containing only scalar values on a single line.
* Types considered scalar here are int, bool, float, string and null.
- * This option is a subset of INLINE_ARRAY, and has no effect when INLINE_ARRAY is used.
+ * This option is a subset of INLINE_ARRAY and INLINE_LITERAL_LIST and has no effect when either is used.
*/
public const INLINE_SCALAR_LIST = 1 << 7;
- /**
- * @deprecated Please use INLINE_SCALAR_LIST instead.
- */
- public const INLINE_NUMERIC_SCALAR_ARRAY = self::INLINE_SCALAR_LIST;
-
/**
* Export static vars defined via `use` as variables.
*/
@@ -80,16 +83,21 @@ final class VarExporter
public const INLINE_ARRAY = 1 << 11;
/**
- * @param mixed $var The variable to export.
- * @param int $options A bitmask of options. Possible values are `VarExporter::*` constants.
- * Combine multiple options with a bitwise OR `|` operator.
- * @param int $indentLevel The base output indentation level.
- *
- * @return string
+ * Formats lists (0-based numeric arrays) containing only literal values on a single line.
+ * Values considered literal are: int, float, string, bool, null, and enum values.
+ * This option is a subset of INLINE_ARRAY and has no effect when INLINE_ARRAY is used.
+ */
+ public const INLINE_LITERAL_LIST = 1 << 12;
+
+ /**
+ * @param mixed $var The variable to export.
+ * @param int $options A bitmask of options. Possible values are `VarExporter::*` constants.
+ * Combine multiple options with a bitwise OR `|` operator.
+ * @param int $indentLevel The base output indentation level.
*
* @throws ExportException
*/
- public static function export($var, int $options = 0, int $indentLevel = 0) : string
+ public static function export(mixed $var, int $options = 0, int $indentLevel = 0): string
{
$exporter = new GenericExporter($options, $indentLevel);
$lines = $exporter->export($var, [], []);
@@ -98,14 +106,15 @@ public static function export($var, int $options = 0, int $indentLevel = 0) : st
$export = implode(PHP_EOL, $lines);
} else {
$firstLine = array_shift($lines);
- $lines = array_map(function ($line) use ($indentLevel) {
- return str_repeat(' ', $indentLevel) . $line;
- }, $lines);
+ $lines = array_map(
+ fn ($line) => str_repeat(' ', $indentLevel) . $line,
+ $lines,
+ );
$export = $firstLine . PHP_EOL . implode(PHP_EOL, $lines);
}
- if ($options & self::ADD_RETURN) {
+ if (($options & self::ADD_RETURN) !== 0) {
return 'return ' . $export . ';' . PHP_EOL;
}
diff --git a/app/vendor/brick/varexporter/tools/ecs/composer.json b/app/vendor/brick/varexporter/tools/ecs/composer.json
new file mode 100644
index 000000000..5298cfffd
--- /dev/null
+++ b/app/vendor/brick/varexporter/tools/ecs/composer.json
@@ -0,0 +1,10 @@
+{
+ "require": {
+ "brick/coding-standard": "v4"
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": false
+ }
+ }
+}
diff --git a/app/vendor/brick/varexporter/tools/ecs/ecs.php b/app/vendor/brick/varexporter/tools/ecs/ecs.php
new file mode 100644
index 000000000..9f00ee7a6
--- /dev/null
+++ b/app/vendor/brick/varexporter/tools/ecs/ecs.php
@@ -0,0 +1,32 @@
+import(__DIR__ . '/vendor/brick/coding-standard/ecs.php');
+
+ $libRootPath = realpath(__DIR__ . '/../../');
+
+ $ecsConfig->paths(
+ [
+ $libRootPath . '/src',
+ $libRootPath . '/tests',
+ __FILE__,
+ ],
+ );
+
+ $ecsConfig->skip([
+ // uses unknown functions etc., let's not touch it
+ $libRootPath . '/tests/ExportClosureTest.php',
+
+ // tests expect a certain order of class elements
+ OrderedClassElementsFixer::class => $libRootPath . '/tests/Classes/Hierarchy/*.php',
+
+ // assertEquals() is used intentionally in assertExportEquals()
+ PhpUnitStrictFixer::class => $libRootPath . 'tests/AbstractTestCase.php',
+ ]);
+};
diff --git a/app/vendor/cakephp-plugins.php b/app/vendor/cakephp-plugins.php
index 8497f738b..7d415483c 100644
--- a/app/vendor/cakephp-plugins.php
+++ b/app/vendor/cakephp-plugins.php
@@ -3,8 +3,10 @@
return [
'plugins' => [
+ 'Authentication' => $baseDir . '/vendor/cakephp/authentication/',
'Bake' => $baseDir . '/vendor/cakephp/bake/',
'Cake/TwigView' => $baseDir . '/vendor/cakephp/twig-view/',
+ 'CoreApi' => $baseDir . '/plugins/CoreApi/',
'CoreAssigner' => $baseDir . '/plugins/CoreAssigner/',
'CoreEnroller' => $baseDir . '/plugins/CoreEnroller/',
'CoreJob' => $baseDir . '/plugins/CoreJob/',
@@ -12,6 +14,8 @@
'DebugKit' => $baseDir . '/vendor/cakephp/debug_kit/',
'EnvSource' => $baseDir . '/plugins/EnvSource/',
'Migrations' => $baseDir . '/vendor/cakephp/migrations/',
- 'TestWidget' => $baseDir . '/plugins/TestWidget/',
+ 'OrcidSource' => $baseDir . '/plugins/OrcidSource/',
+ 'SshKeyAuthenticator' => $baseDir . '/plugins/SshKeyAuthenticator/',
+ 'TermsAgreer' => $baseDir . '/plugins/TermsAgreer/',
],
];
diff --git a/app/vendor/cakephp/authentication/Dockerfile b/app/vendor/cakephp/authentication/Dockerfile
new file mode 100644
index 000000000..7acfb27ee
--- /dev/null
+++ b/app/vendor/cakephp/authentication/Dockerfile
@@ -0,0 +1,26 @@
+# Basic docker based environment
+# Necessary to trick dokku into building the documentation
+# using dockerfile instead of herokuish
+FROM ubuntu:22.04
+
+# Add basic tools
+RUN apt-get update && \
+ apt-get install -y build-essential \
+ software-properties-common \
+ curl \
+ git \
+ libxml2 \
+ libffi-dev \
+ libssl-dev
+
+# Prevent interactive timezone input
+ENV DEBIAN_FRONTEND=noninteractive
+RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \
+ apt-get update && \
+ apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite
+
+WORKDIR /code
+
+VOLUME ["/code"]
+
+CMD [ '/bin/bash' ]
diff --git a/app/vendor/cakephp/authentication/LICENSE.txt b/app/vendor/cakephp/authentication/LICENSE.txt
new file mode 100644
index 000000000..ce7073bdf
--- /dev/null
+++ b/app/vendor/cakephp/authentication/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (C) 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/authentication/composer.json b/app/vendor/cakephp/authentication/composer.json
new file mode 100644
index 000000000..8f74bcc9d
--- /dev/null
+++ b/app/vendor/cakephp/authentication/composer.json
@@ -0,0 +1,80 @@
+{
+ "name": "cakephp/authentication",
+ "description": "Authentication plugin for CakePHP",
+ "license": "MIT",
+ "type": "cakephp-plugin",
+ "keywords": [
+ "auth",
+ "authentication",
+ "cakephp",
+ "middleware"
+ ],
+ "authors": [
+ {
+ "name": "CakePHP Community",
+ "homepage": "https://github.com/cakephp/authentication/graphs/contributors"
+ }
+ ],
+ "homepage": "https://cakephp.org",
+ "support": {
+ "issues": "https://github.com/cakephp/authentication/issues",
+ "forum": "https://discourse.cakephp.org/",
+ "source": "https://github.com/cakephp/authentication",
+ "docs": "https://book.cakephp.org/authentication/3/en/"
+ },
+ "require": {
+ "php": ">=8.1",
+ "cakephp/http": "^5.0",
+ "laminas/laminas-diactoros": "^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0"
+ },
+ "require-dev": {
+ "cakephp/cakephp": "^5.1.0",
+ "cakephp/cakephp-codesniffer": "^5.0",
+ "firebase/php-jwt": "^6.2",
+ "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.4"
+ },
+ "suggest": {
+ "ext-ldap": "Make sure this php extension is installed and enabled on your system if you want to use the built-in LDAP adapter for \"LdapIdentifier\".",
+ "cakephp/cakephp": "Install full core to use \"CookieAuthenticator\".",
+ "cakephp/orm": "To use \"OrmResolver\" (Not needed separately if using full CakePHP framework).",
+ "cakephp/utility": "Provides CakePHP security methods. Required for the JWT adapter and Legacy password hasher.",
+ "firebase/php-jwt": "If you want to use the JWT adapter add this dependency"
+ },
+ "autoload": {
+ "psr-4": {
+ "Authentication\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Authentication\\Test\\": "tests/",
+ "Cake\\Test\\": "vendor/cakephp/cakephp/tests/",
+ "TestApp\\": "tests/test_app/TestApp/",
+ "TestPlugin\\": "tests/test_app/Plugin/TestPlugin/src/"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ },
+ "sort-packages": true
+ },
+ "scripts": {
+ "check": [
+ "@cs-check",
+ "@test"
+ ],
+ "cs-check": "phpcs --colors -p src/ tests/",
+ "cs-fix": "phpcbf --colors -p src/ tests/",
+ "phpstan": "tools/phpstan analyse",
+ "stan": "@phpstan",
+ "stan-baseline": "tools/phpstan --generate-baseline",
+ "stan-setup": "phive install",
+ "test": "phpunit",
+ "test-coverage": "phpunit --coverage-clover=clover.xml"
+ }
+}
diff --git a/app/vendor/cakephp/authentication/phpunit.xml.dist b/app/vendor/cakephp/authentication/phpunit.xml.dist
new file mode 100644
index 000000000..b1a2a3c81
--- /dev/null
+++ b/app/vendor/cakephp/authentication/phpunit.xml.dist
@@ -0,0 +1,35 @@
+
+
+
+
+
+ tests/TestCase/
+
+
+
+
+
+
+
+
+
+ src/
+
+
+ src/Identifier/Ldap/ExtensionAdapter.php
+
+
+
+
+
+
+
+
diff --git a/app/vendor/cakephp/authentication/readme.md b/app/vendor/cakephp/authentication/readme.md
new file mode 100644
index 000000000..c63fb39c2
--- /dev/null
+++ b/app/vendor/cakephp/authentication/readme.md
@@ -0,0 +1,42 @@
+# CakePHP Authentication
+
+[](https://github.com/cakephp/authentication/actions/workflows/ci.yml)
+[](https://packagist.org/packages/cakephp/authentication)
+[](https://packagist.org/packages/cakephp/authentication/stats)
+[](https://coveralls.io/r/cakephp/authentication?branch=master)
+[](LICENSE)
+
+[PSR7](https://www.php-fig.org/psr/psr-7/) Middleware authentication stack for the CakePHP framework.
+
+Don't know what middleware is? [Check the CakePHP documentation](https://book.cakephp.org/4/en/controllers/middleware.html) and additionally [read this.](https://philsturgeon.uk/php/2016/05/31/why-care-about-php-middleware/)
+
+## Authentication, not Authorization
+
+This plugin intends to provide a framework around authentication and user
+identification. Authorization is a [separate
+concern](https://en.wikipedia.org/wiki/Separation_of_concerns) that has been
+packaged into a separate [authorization plugin](https://github.com/cakephp/authorization).
+
+## Installation
+
+You can install this plugin into your CakePHP application using
+[composer](https://getcomposer.org):
+
+```
+composer require cakephp/authentication
+```
+
+Then load the plugin:
+```
+bin/cake plugin load Authentication
+```
+
+## Documentation
+
+Documentation for this plugin can be found in the [CakePHP Cookbook](https://book.cakephp.org/authentication/3/en/).
+
+## IDE compatibility improvements
+
+There are IdeHelper tasks in [IdeHelperExtra plugin](https://github.com/dereuromark/cakephp-ide-helper-extra/) to provide auto-complete:
+- `AuthenticationService::loadAuthenticator()`
+- `IdentifierCollection::load()`
diff --git a/app/vendor/cakephp/authentication/src/AbstractCollection.php b/app/vendor/cakephp/authentication/src/AbstractCollection.php
new file mode 100644
index 000000000..7ce991401
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/AbstractCollection.php
@@ -0,0 +1,64 @@
+
+ */
+abstract class AbstractCollection extends ObjectRegistry
+{
+ use InstanceConfigTrait;
+
+ /**
+ * Config array.
+ *
+ * @var array
+ */
+ protected array $_defaultConfig = [];
+
+ /**
+ * Constructor
+ *
+ * @param array $config Configuration
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+
+ foreach ($config as $key => $value) {
+ if (is_int($key)) {
+ $this->load($value);
+ continue;
+ }
+ $this->load($key, $value);
+ }
+ }
+
+ /**
+ * Returns true if a collection is empty.
+ *
+ * @return bool
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->_loaded);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/AuthenticationPlugin.php b/app/vendor/cakephp/authentication/src/AuthenticationPlugin.php
new file mode 100644
index 000000000..3400bf096
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/AuthenticationPlugin.php
@@ -0,0 +1,45 @@
+ [
+ * 'enabled' => true, // Enable validation (default: false for BC)
+ * 'maxDepth' => 1, // Max nested "redirect=" parameters (default: 1)
+ * 'maxEncodingLevels' => 1, // Max percent-encoding levels (default: 1)
+ * 'maxLength' => 2000, // Max URL length in characters (default: 2000)
+ * ]
+ * ```
+ *
+ * ### Example:
+ *
+ * ```
+ * $service = new AuthenticationService([
+ * 'authenticators' => [
+ * 'Authentication.Form' => [
+ * 'identifier' => 'Authentication.Password',
+ * ],
+ * ],
+ * ]);
+ * ```
+ *
+ * @var array
+ */
+ protected array $_defaultConfig = [
+ 'authenticators' => [],
+ 'identifiers' => [],
+ 'identityClass' => Identity::class,
+ 'identityAttribute' => 'identity',
+ 'queryParam' => null,
+ 'unauthenticatedRedirect' => null,
+ 'redirectValidation' => [
+ 'enabled' => false, // Disabled by default for backward compatibility
+ 'maxDepth' => 1,
+ 'maxEncodingLevels' => 1,
+ 'maxLength' => 2000,
+ ],
+ ];
+
+ /**
+ * Constructor
+ *
+ * @param array $config Configuration options.
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+ }
+
+ /**
+ * Access the identifier collection
+ *
+ * @return \Authentication\Identifier\IdentifierCollection
+ */
+ public function identifiers(): IdentifierCollection
+ {
+ if ($this->_identifiers === null) {
+ $this->_identifiers = new IdentifierCollection($this->getConfig('identifiers'));
+ }
+
+ return $this->_identifiers;
+ }
+
+ /**
+ * Access the authenticator collection
+ *
+ * @return \Authentication\Authenticator\AuthenticatorCollection
+ */
+ public function authenticators(): AuthenticatorCollection
+ {
+ if ($this->_authenticators === null) {
+ $identifiers = $this->identifiers();
+ $authenticators = $this->getConfig('authenticators');
+ $this->_authenticators = new AuthenticatorCollection($identifiers, $authenticators);
+ }
+
+ return $this->_authenticators;
+ }
+
+ /**
+ * Loads an authenticator.
+ *
+ * @param string $name Name or class name.
+ * @param array $config Authenticator configuration.
+ * @return \Authentication\Authenticator\AuthenticatorInterface
+ */
+ public function loadAuthenticator(string $name, array $config = []): AuthenticatorInterface
+ {
+ return $this->authenticators()->load($name, $config);
+ }
+
+ /**
+ * Loads an identifier.
+ *
+ * @param string $name Name or class name.
+ * @param array $config Identifier configuration.
+ * @return \Authentication\Identifier\IdentifierInterface Identifier instance
+ * @deprecated 3.3.0: loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator.
+ */
+ public function loadIdentifier(string $name, array $config = []): IdentifierInterface
+ {
+ deprecationWarning(
+ '3.3.0',
+ 'loadIdentifier() usage is deprecated. Directly pass `\'identifier\'` config to the Authenticator.',
+ );
+
+ return $this->identifiers()->load($name, $config);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @return \Authentication\Authenticator\ResultInterface The result object. If none of the adapters was a success
+ * the last failed result is returned.
+ * @throws \RuntimeException Throws a runtime exception when no authenticators are loaded.
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $result = null;
+ /** @var \Authentication\Authenticator\AuthenticatorInterface $authenticator */
+ foreach ($this->authenticators() as $authenticator) {
+ $result = $authenticator->authenticate($request);
+ if ($result->isValid()) {
+ $this->_successfulAuthenticator = $authenticator;
+
+ return $this->_result = $result;
+ }
+
+ if ($authenticator instanceof StatelessInterface) {
+ $authenticator->unauthorizedChallenge($request);
+ }
+ }
+
+ if ($result === null) {
+ throw new RuntimeException(
+ 'No authenticators loaded. You need to load at least one authenticator.',
+ );
+ }
+
+ $this->_successfulAuthenticator = null;
+
+ return $this->_result = $result;
+ }
+
+ /**
+ * Clears the identity from authenticators that store them and the request
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Message\ResponseInterface $response The response.
+ * @return array Return an array containing the request and response objects.
+ * @return array{request: \Psr\Http\Message\ServerRequestInterface, response: \Psr\Http\Message\ResponseInterface}
+ */
+ public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array
+ {
+ foreach ($this->authenticators() as $authenticator) {
+ if ($authenticator instanceof PersistenceInterface) {
+ if ($authenticator instanceof ImpersonationInterface && $authenticator->isImpersonating($request)) {
+ $stopImpersonationResult = $authenticator->stopImpersonating($request, $response);
+ ['request' => $request, 'response' => $response] = $stopImpersonationResult;
+ }
+ $result = $authenticator->clearIdentity($request, $response);
+ ['request' => $request, 'response' => $response] = $result;
+ }
+ }
+ $this->_successfulAuthenticator = null;
+
+ return [
+ 'request' => $request->withoutAttribute($this->getConfig('identityAttribute')),
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * Sets identity data and persists it in the authenticators that support it.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @param \Psr\Http\Message\ResponseInterface $response The response.
+ * @param \ArrayAccess|array $identity Identity data.
+ * @return array{request: \Psr\Http\Message\ServerRequestInterface, response: \Psr\Http\Message\ResponseInterface}
+ */
+ public function persistIdentity(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ArrayAccess|array $identity,
+ ): array {
+ foreach ($this->authenticators() as $authenticator) {
+ if ($authenticator instanceof PersistenceInterface) {
+ $result = $authenticator->persistIdentity($request, $response, $identity);
+ $request = $result['request'];
+ $response = $result['response'];
+ }
+ }
+
+ $identity = $this->buildIdentity($identity);
+
+ return [
+ 'request' => $request->withAttribute($this->getConfig('identityAttribute'), $identity),
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * Gets the successful authenticator instance if one was successful after calling authenticate.
+ *
+ * @return \Authentication\Authenticator\AuthenticatorInterface|null
+ */
+ public function getAuthenticationProvider(): ?AuthenticatorInterface
+ {
+ return $this->_successfulAuthenticator;
+ }
+
+ /**
+ * Convenient method to gets the successful identifier instance.
+ *
+ * @return \Authentication\Identifier\IdentifierInterface|null
+ */
+ public function getIdentificationProvider(): ?IdentifierInterface
+ {
+ if ($this->_successfulAuthenticator === null) {
+ return null;
+ }
+
+ $identifier = $this->_successfulAuthenticator->getIdentifier();
+ if ($identifier instanceof IdentifierCollection) {
+ return $identifier->getIdentificationProvider();
+ }
+
+ return $identifier;
+ }
+
+ /**
+ * Gets the result of the last authenticate() call.
+ *
+ * @return \Authentication\Authenticator\ResultInterface|null Authentication result interface
+ */
+ public function getResult(): ?ResultInterface
+ {
+ return $this->_result;
+ }
+
+ /**
+ * Gets an identity object
+ *
+ * @return \Authentication\IdentityInterface|null
+ */
+ public function getIdentity(): ?IdentityInterface
+ {
+ if ($this->_result === null) {
+ return null;
+ }
+
+ $identityData = $this->_result->getData();
+ if (!$this->_result->isValid() || $identityData === null) {
+ return null;
+ }
+
+ return $this->buildIdentity($identityData);
+ }
+
+ /**
+ * Return the name of the identity attribute.
+ *
+ * @return string
+ */
+ public function getIdentityAttribute(): string
+ {
+ return $this->getConfig('identityAttribute');
+ }
+
+ /**
+ * Builds the identity object
+ *
+ * @param \ArrayAccess|array $identityData Identity data
+ * @return \Authentication\IdentityInterface
+ */
+ public function buildIdentity(ArrayAccess|array $identityData): IdentityInterface
+ {
+ if ($identityData instanceof IdentityInterface) {
+ return $identityData;
+ }
+
+ $class = $this->getConfig('identityClass');
+
+ if (is_callable($class)) {
+ $identity = $class($identityData);
+ } else {
+ $identity = new $class($identityData);
+ }
+
+ if (!($identity instanceof IdentityInterface)) {
+ throw new RuntimeException(sprintf(
+ 'Object `%s` does not implement `%s`',
+ get_class($identity),
+ IdentityInterface::class,
+ ));
+ }
+
+ return $identity;
+ }
+
+ /**
+ * Return the URL to redirect unauthenticated users to.
+ *
+ * If the `unauthenticatedRedirect` option is not set,
+ * this method will return null.
+ *
+ * If the `queryParam` option is set a query parameter
+ * will be appended with the denied URL path.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return string|null
+ */
+ public function getUnauthenticatedRedirectUrl(ServerRequestInterface $request): ?string
+ {
+ $target = $this->getConfig('unauthenticatedRedirect');
+ if ($target === null) {
+ return null;
+ }
+
+ if (is_array($target) && class_exists(Router::class)) {
+ $target = Router::url($target);
+ }
+
+ if ($request->getMethod() !== 'GET') {
+ return $target;
+ }
+
+ $param = $this->getConfig('queryParam');
+ if ($param === null) {
+ return $target;
+ }
+
+ $uri = $request->getUri();
+ $redirect = $uri->getPath();
+ if ($uri->getQuery()) {
+ $redirect .= '?' . $uri->getQuery();
+ }
+ $query = urlencode($param) . '=' . urlencode($redirect);
+
+ /** @var array $url */
+ $url = parse_url($target);
+ if (isset($url['query']) && strlen($url['query'])) {
+ $url['query'] .= '&' . $query;
+ } else {
+ $url['query'] = $query;
+ }
+ $fragment = isset($url['fragment']) ? '#' . $url['fragment'] : '';
+ $url['path'] = $url['path'] ?? '/';
+
+ return $url['path'] . '?' . $url['query'] . $fragment;
+ }
+
+ /**
+ * Return the URL that an authenticated user came from or null.
+ *
+ * This reads from the URL parameter defined in the `queryParam` option.
+ * Will return null if this parameter doesn't exist or is invalid.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return string|null
+ */
+ public function getLoginRedirect(ServerRequestInterface $request): ?string
+ {
+ $redirectParam = $this->getConfig('queryParam');
+ $params = $request->getQueryParams();
+ if (
+ empty($redirectParam) ||
+ !isset($params[$redirectParam]) ||
+ strlen($params[$redirectParam]) === 0
+ ) {
+ return null;
+ }
+
+ $parsed = parse_url($params[$redirectParam]);
+ if ($parsed === false) {
+ return null;
+ }
+ if (!empty($parsed['host']) || !empty($parsed['scheme'])) {
+ return null;
+ }
+ $parsed += ['path' => '/', 'query' => ''];
+ if (strlen($parsed['path']) && $parsed['path'][0] !== '/') {
+ $parsed['path'] = "/{$parsed['path']}";
+ }
+ if ($parsed['query']) {
+ $parsed['query'] = "?{$parsed['query']}";
+ }
+
+ $redirect = $parsed['path'] . $parsed['query'];
+
+ // Validate redirect to prevent loops if enabled
+ return $this->validateRedirect($redirect);
+ }
+
+ /**
+ * Validates a redirect URL to prevent loops and malicious patterns
+ *
+ * This method can be overridden in subclasses to implement custom validation logic.
+ *
+ * @param string $redirect The redirect URL to validate
+ * @return string|null The validated URL or null if invalid
+ */
+ protected function validateRedirect(string $redirect): ?string
+ {
+ $config = $this->getConfig('redirectValidation');
+
+ // If validation is disabled, return the URL as-is (backward compatibility)
+ if (!$config['enabled']) {
+ return $redirect;
+ }
+
+ $decodedUrl = urldecode($redirect);
+
+ // Check for nested redirect parameters
+ $redirectCount = substr_count($decodedUrl, 'redirect=');
+ if ($redirectCount >= $config['maxDepth']) {
+ return null;
+ }
+
+ // Check for multiple encoding levels (e.g., %25 = percent-encoded %)
+ $encodingCount = substr_count($redirect, '%25');
+ if ($encodingCount >= $config['maxEncodingLevels']) {
+ return null;
+ }
+
+ // Check URL length to prevent DOS attacks
+ if (strlen($redirect) > $config['maxLength']) {
+ return null;
+ }
+
+ return $redirect;
+ }
+
+ /**
+ * Impersonates a user
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @param \Psr\Http\Message\ResponseInterface $response The response
+ * @param \ArrayAccess $impersonator User who impersonates
+ * @param \ArrayAccess $impersonated User impersonated
+ * @return array
+ */
+ public function impersonate(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ArrayAccess $impersonator,
+ ArrayAccess $impersonated,
+ ): array {
+ $provider = $this->getImpersonationProvider();
+
+ return $provider->impersonate($request, $response, $impersonator, $impersonated);
+ }
+
+ /**
+ * Stops impersonation
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @param \Psr\Http\Message\ResponseInterface $response The response
+ * @return array
+ */
+ public function stopImpersonating(ServerRequestInterface $request, ResponseInterface $response): array
+ {
+ $provider = $this->getImpersonationProvider();
+
+ return $provider->stopImpersonating($request, $response);
+ }
+
+ /**
+ * Returns true if impersonation is being done
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return bool
+ */
+ public function isImpersonating(ServerRequestInterface $request): bool
+ {
+ $provider = $this->getImpersonationProvider();
+
+ return $provider->isImpersonating($request);
+ }
+
+ /**
+ * Get impersonation provider
+ *
+ * @return \Authentication\Authenticator\ImpersonationInterface
+ * @throws \InvalidArgumentException
+ */
+ protected function getImpersonationProvider(): ImpersonationInterface
+ {
+ $provider = $this->getAuthenticationProvider();
+ if ($provider === null) {
+ throw new InvalidArgumentException('No AuthenticationProvider present.');
+ }
+ if (!($provider instanceof ImpersonationInterface)) {
+ $className = get_class($provider);
+ throw new InvalidArgumentException(
+ "The {$className} Provider must implement ImpersonationInterface in order to use impersonation.",
+ );
+ }
+
+ return $provider;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/AuthenticationServiceInterface.php b/app/vendor/cakephp/authentication/src/AuthenticationServiceInterface.php
new file mode 100644
index 000000000..ade15c28f
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/AuthenticationServiceInterface.php
@@ -0,0 +1,98 @@
+ $config Authenticator configuration.
+ * @return \Authentication\Authenticator\AuthenticatorInterface
+ */
+ public function loadAuthenticator(string $name, array $config = []): AuthenticatorInterface;
+
+ /**
+ * Loads an identifier.
+ *
+ * @param string $name Name or class name.
+ * @param array $config Identifier configuration.
+ * @return \Authentication\Identifier\IdentifierInterface
+ * @deprecated 3.3.0: loadIdentifier() usage is deprecated. Directly pass Identifier to Authenticator.
+ */
+ public function loadIdentifier(string $name, array $config = []): IdentifierInterface;
+
+ /**
+ * Authenticate the request against the configured authentication adapters.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request.
+ * @return \Authentication\Authenticator\ResultInterface The result object. If none of the adapters was a success
+ * the last failed result is returned.
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface;
+
+ /**
+ * Gets an identity object or null if identity has not been resolved.
+ *
+ * @return \Authentication\IdentityInterface|null
+ */
+ public function getIdentity(): ?IdentityInterface;
+
+ /**
+ * Gets the successful authenticator instance if one was successful after calling authenticate
+ *
+ * @return \Authentication\Authenticator\AuthenticatorInterface|null
+ */
+ public function getAuthenticationProvider(): ?AuthenticatorInterface;
+
+ /**
+ * Gets the result of the last authenticate() call.
+ *
+ * @return \Authentication\Authenticator\ResultInterface|null Authentication result interface
+ */
+ public function getResult(): ?ResultInterface;
+
+ /**
+ * Return the name of the identity attribute.
+ *
+ * @return string
+ */
+ public function getIdentityAttribute(): string;
+
+ /**
+ * Return the URL to redirect unauthenticated users to.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return string|null
+ */
+ public function getUnauthenticatedRedirectUrl(ServerRequestInterface $request): ?string;
+
+ /**
+ * Return the URL that an authenticated user came from or null.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return string|null
+ */
+ public function getLoginRedirect(ServerRequestInterface $request): ?string;
+}
diff --git a/app/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php b/app/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php
new file mode 100644
index 000000000..a50bdc64c
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/AuthenticationServiceProviderInterface.php
@@ -0,0 +1,33 @@
+ [
+ AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
+ AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
+ ],
+ ];
+
+ /**
+ * Identifier or identifiers collection.
+ *
+ * @var \Authentication\Identifier\IdentifierInterface
+ */
+ protected IdentifierInterface $_identifier;
+
+ /**
+ * Constructor
+ *
+ * @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection.
+ * @param array $config Configuration settings.
+ */
+ public function __construct(IdentifierInterface $identifier, array $config = [])
+ {
+ $this->_identifier = $identifier;
+ $this->setConfig($config);
+ }
+
+ /**
+ * Gets the identifier.
+ *
+ * @return \Authentication\Identifier\IdentifierInterface
+ */
+ public function getIdentifier(): IdentifierInterface
+ {
+ return $this->_identifier;
+ }
+
+ /**
+ * Sets the identifier.
+ *
+ * @param \Authentication\Identifier\IdentifierInterface $identifier IdentifierInterface instance.
+ * @return $this
+ */
+ public function setIdentifier(IdentifierInterface $identifier)
+ {
+ $this->_identifier = $identifier;
+
+ return $this;
+ }
+
+ /**
+ * Authenticate a user based on the request information.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request Request to get authentication information from.
+ * @return \Authentication\Authenticator\ResultInterface Returns a result object.
+ */
+ abstract public function authenticate(ServerRequestInterface $request): ResultInterface;
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/AuthenticationRequiredException.php b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticationRequiredException.php
new file mode 100644
index 000000000..017074ce1
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticationRequiredException.php
@@ -0,0 +1,73 @@
+|string>
+ */
+ protected array $headers = [];
+
+ /**
+ * @var string
+ */
+ protected string $body = '';
+
+ /**
+ * Constructor
+ *
+ * @param array $headers The headers that should be sent in the unauthorized challenge response.
+ * @param string $body The response body that should be sent in the challenge response.
+ * @param int $code The exception code that will be used as a HTTP status code
+ */
+ public function __construct(array $headers, string $body = '', int $code = 401)
+ {
+ parent::__construct(__d('authentication', 'Authentication is required to continue'), $code);
+ $this->headers = $headers;
+ $this->body = $body;
+ }
+
+ /**
+ * Get the headers.
+ *
+ * @return array
+ */
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Get the body.
+ *
+ * @return string
+ */
+ public function getBody(): string
+ {
+ return $this->body;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorCollection.php b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorCollection.php
new file mode 100644
index 000000000..5e4249d01
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorCollection.php
@@ -0,0 +1,106 @@
+
+ */
+class AuthenticatorCollection extends AbstractCollection
+{
+ /**
+ * Identifier collection.
+ *
+ * @var \Authentication\Identifier\IdentifierCollection
+ */
+ protected IdentifierCollection $_identifiers;
+
+ /**
+ * Constructor.
+ *
+ * @param \Authentication\Identifier\IdentifierCollection $identifiers Identifiers collection.
+ * @param array $config Config array.
+ */
+ public function __construct(IdentifierCollection $identifiers, array $config = [])
+ {
+ $this->_identifiers = $identifiers;
+ if ($identifiers->count() > 0) {
+ deprecationWarning(
+ '3.3.0',
+ 'loadIdentifier() usage is deprecated. Directly pass `\'identifier\'` config to the Authenticator.',
+ );
+ }
+
+ parent::__construct($config);
+ }
+
+ /**
+ * Creates authenticator instance.
+ *
+ * @param \Authentication\Authenticator\AuthenticatorInterface|class-string<\Authentication\Authenticator\AuthenticatorInterface> $class Authenticator class.
+ * @param string $alias Authenticator alias.
+ * @param array $config Config array.
+ * @return \Authentication\Authenticator\AuthenticatorInterface
+ * @throws \RuntimeException
+ */
+ protected function _create(object|string $class, string $alias, array $config): AuthenticatorInterface
+ {
+ if (is_string($class)) {
+ if (!empty($config['identifier'])) {
+ $this->_identifiers = new IdentifierCollection((array)$config['identifier']);
+ }
+
+ return new $class($this->_identifiers, $config);
+ }
+
+ return $class;
+ }
+
+ /**
+ * Resolves authenticator class name.
+ *
+ * @param string $class Class name to be resolved.
+ * @return class-string<\Authentication\Authenticator\AuthenticatorInterface>|null
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ /** @var class-string<\Authentication\Authenticator\AuthenticatorInterface>|null */
+ return App::className($class, 'Authenticator', 'Authenticator');
+ }
+
+ /**
+ * @param string $class Missing class.
+ * @param string|null $plugin Class plugin.
+ * @return void
+ * @throws \RuntimeException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ if ($plugin) {
+ $class = $plugin . '.' . $class;
+ }
+
+ $message = sprintf('Authenticator class `%s` was not found.', $class);
+
+ throw new RuntimeException($message);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorInterface.php
new file mode 100644
index 000000000..603ddf7e4
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/AuthenticatorInterface.php
@@ -0,0 +1,33 @@
+ null,
+ 'urlChecker' => 'Authentication.Default',
+ 'rememberMeField' => 'remember_me',
+ 'fields' => [
+ AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
+ AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
+ ],
+ 'cookie' => [
+ 'name' => 'CookieAuth',
+ ],
+ 'passwordHasher' => 'Authentication.Default',
+ 'salt' => true,
+ ];
+
+ /**
+ * Gets the identifier, loading a default Password identifier if none configured.
+ *
+ * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator().
+ *
+ * @return \Authentication\Identifier\IdentifierInterface
+ */
+ public function getIdentifier(): IdentifierInterface
+ {
+ if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) {
+ $identifierConfig = [];
+ if ($this->getConfig('fields')) {
+ $identifierConfig['fields'] = $this->getConfig('fields');
+ }
+ $this->_identifier->load('Authentication.Password', $identifierConfig);
+ }
+
+ return $this->_identifier;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $cookies = $request->getCookieParams();
+ $cookieName = $this->getConfig('cookie.name');
+ if (!isset($cookies[$cookieName])) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [
+ 'Login credentials not found',
+ ]);
+ }
+
+ if (is_array($cookies[$cookieName])) {
+ $token = $cookies[$cookieName];
+ } else {
+ $token = json_decode($cookies[$cookieName], true);
+ }
+
+ if ($token === null || count($token) !== 2) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, [
+ 'Cookie token is invalid.',
+ ]);
+ }
+
+ [$username, $tokenHash] = $token;
+
+ $identifier = $this->getIdentifier();
+ $identity = $identifier->identify(compact('username'));
+
+ if (!$identity) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $identifier->getErrors());
+ }
+
+ if (!$this->_checkToken($identity, $tokenHash)) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_INVALID, [
+ 'Cookie token does not match',
+ ]);
+ }
+
+ return new Result($identity, Result::SUCCESS);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array
+ {
+ $field = $this->getConfig('rememberMeField');
+ $bodyData = $request->getParsedBody();
+
+ if (!$this->_checkUrl($request) || !is_array($bodyData) || empty($bodyData[$field])) {
+ return [
+ 'request' => $request,
+ 'response' => $response,
+ ];
+ }
+
+ $value = $this->_createToken($identity);
+ $cookie = $this->_createCookie($value);
+
+ return [
+ 'request' => $request,
+ 'response' => $response->withAddedHeader('Set-Cookie', $cookie->toHeaderValue()),
+ ];
+ }
+
+ /**
+ * Creates a plain part of a cookie token.
+ *
+ * Returns concatenated username, password hash, and HMAC signature.
+ *
+ * @param \ArrayAccess|array $identity Identity data.
+ * @return string
+ */
+ protected function _createPlainToken(ArrayAccess|array $identity): string
+ {
+ $usernameField = $this->getConfig('fields.username');
+ $passwordField = $this->getConfig('fields.password');
+
+ if ($identity[$usernameField] === null || $identity[$passwordField] === null) {
+ throw new InvalidArgumentException(
+ sprintf('Fields %s cannot be found in entity', '`' . $usernameField . '`/`' . $passwordField . '`'),
+ );
+ }
+
+ $value = $identity[$usernameField] . $identity[$passwordField];
+ $salt = $this->getConfig('salt', '');
+
+ if ($salt === false) {
+ return $value;
+ }
+ if ($salt === true) {
+ $salt = Security::getSalt();
+ } elseif (!is_string($salt) || $salt === '') {
+ throw new InvalidArgumentException('Salt must be a non-empty string.');
+ }
+
+ $hmac = hash_hmac('sha1', $value, $salt);
+ // Instead of appending the plain salt, we create a hash. This limits the chance of the salt being leaked.
+
+ return $value . $hmac;
+ }
+
+ /**
+ * Creates a full cookie token serialized as a JSON sting.
+ *
+ * Cookie token consists of a username and hashed username + password hash.
+ *
+ * @param \ArrayAccess|array $identity Identity data.
+ * @return string
+ * @throws \JsonException
+ */
+ protected function _createToken(ArrayAccess|array $identity): string
+ {
+ $plain = $this->_createPlainToken($identity);
+ $hash = $this->getPasswordHasher()->hash($plain);
+
+ $usernameField = $this->getConfig('fields.username');
+
+ return json_encode([$identity[$usernameField], $hash], JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * Checks whether a token hash matches the identity data.
+ *
+ * @param \ArrayAccess|array $identity Identity data.
+ * @param string $tokenHash Hashed part of a cookie token.
+ * @return bool
+ */
+ protected function _checkToken(ArrayAccess|array $identity, string $tokenHash): bool
+ {
+ $plain = $this->_createPlainToken($identity);
+
+ return $this->getPasswordHasher()->check($plain, $tokenHash);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array
+ {
+ $cookie = $this->_createCookie('')->withExpired();
+
+ return [
+ 'request' => $request,
+ 'response' => $response->withAddedHeader('Set-Cookie', $cookie->toHeaderValue()),
+ ];
+ }
+
+ /**
+ * Creates a cookie instance with configured defaults.
+ *
+ * @param mixed $value Cookie value.
+ * @return \Cake\Http\Cookie\CookieInterface
+ */
+ protected function _createCookie(mixed $value): CookieInterface
+ {
+ $options = $this->getConfig('cookie');
+ $name = $options['name'];
+ unset($options['name']);
+
+ return Cookie::create(
+ $name,
+ $value,
+ $options,
+ );
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/EnvironmentAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/EnvironmentAuthenticator.php
new file mode 100644
index 000000000..40959a00f
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/EnvironmentAuthenticator.php
@@ -0,0 +1,164 @@
+ null,
+ 'urlChecker' => 'Authentication.Default',
+ 'fields' => [],
+ 'optionalFields' => [],
+ ];
+
+ /**
+ * Get values from the environment variables configured by `fields`.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return array|null server params defined by `fields` or null if a field is missing.
+ */
+ protected function _getData(ServerRequestInterface $request): ?array
+ {
+ $fields = $this->_config['fields'];
+ $params = $request->getServerParams();
+
+ $data = [];
+ foreach ($fields as $field) {
+ if (!isset($params[$field])) {
+ return null;
+ }
+
+ $value = $params[$field];
+ if (!is_string($value) || !strlen($value)) {
+ return null;
+ }
+
+ $data[$field] = $value;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get values from the environment variables configured by `optionalFields`.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return array server params defined by optionalFields.
+ */
+ protected function _getOptionalData(ServerRequestInterface $request): array
+ {
+ $fields = $this->_config['optionalFields'];
+ $params = $request->getServerParams();
+
+ $data = [];
+ foreach ($fields as $field) {
+ if (isset($params[$field])) {
+ $data[$field] = $params[$field];
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Prepares the error object for a login URL error
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ protected function _buildLoginUrlErrorResult(ServerRequestInterface $request): ResultInterface
+ {
+ $uri = $request->getUri();
+ $base = $request->getAttribute('base');
+ if ($base !== null) {
+ $uri = $uri->withPath((string)$base . $uri->getPath());
+ }
+
+ $checkFullUrl = $this->getConfig('urlChecker.checkFullUrl', false);
+ if ($checkFullUrl) {
+ $uri = (string)$uri;
+ } else {
+ $uri = $uri->getPath();
+ }
+
+ $loginUrls = (array)$this->getConfig('loginUrl');
+ foreach ($loginUrls as $key => $loginUrl) {
+ if (is_array($loginUrl)) {
+ $loginUrls[$key] = Router::url($loginUrl);
+ }
+ }
+
+ $errors = [
+ sprintf(
+ 'Login URL `%s` did not match `%s`.',
+ $uri,
+ implode('` or `', $loginUrls),
+ ),
+ ];
+
+ return new Result(null, Result::FAILURE_OTHER, $errors);
+ }
+
+ /**
+ * Authenticates the identity contained in a request.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ if (!$this->_checkUrl($request)) {
+ return $this->_buildLoginUrlErrorResult($request);
+ }
+ $data = $this->_getData($request);
+ if (!$data) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [
+ 'Environment credentials not found',
+ ]);
+ }
+
+ $data = array_merge($this->_getOptionalData($request), $data);
+
+ $user = $this->_identifier->identify($data);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $this->_identifier->getErrors());
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/FormAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/FormAuthenticator.php
new file mode 100644
index 000000000..781b4e70e
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/FormAuthenticator.php
@@ -0,0 +1,173 @@
+ null,
+ 'urlChecker' => 'Authentication.Default',
+ 'fields' => [
+ AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
+ AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
+ ],
+ ];
+
+ /**
+ * Gets the identifier, loading a default Password identifier if none configured.
+ *
+ * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator().
+ *
+ * @return \Authentication\Identifier\IdentifierInterface
+ */
+ public function getIdentifier(): IdentifierInterface
+ {
+ if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) {
+ $identifierConfig = [];
+ if ($this->getConfig('fields')) {
+ $identifierConfig['fields'] = $this->getConfig('fields');
+ }
+ $this->_identifier->load('Authentication.Password', $identifierConfig);
+ }
+
+ return $this->_identifier;
+ }
+
+ /**
+ * Checks the fields to ensure they are supplied.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return array|null Username and password retrieved from a request body.
+ */
+ protected function _getData(ServerRequestInterface $request): ?array
+ {
+ $fields = $this->_config['fields'];
+ /** @var array $body */
+ $body = $request->getParsedBody();
+
+ $data = [];
+ foreach ($fields as $key => $field) {
+ if (!isset($body[$field])) {
+ return null;
+ }
+
+ $value = $body[$field];
+ if (!is_string($value) || !strlen($value)) {
+ return null;
+ }
+
+ $data[$key] = $value;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Prepares the error object for a login URL error
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ protected function _buildLoginUrlErrorResult(ServerRequestInterface $request): ResultInterface
+ {
+ $uri = $request->getUri();
+ $base = $request->getAttribute('base');
+ if ($base !== null) {
+ $uri = $uri->withPath((string)$base . $uri->getPath());
+ }
+
+ $checkFullUrl = $this->getConfig('urlChecker.checkFullUrl', false);
+ if ($checkFullUrl) {
+ $uri = (string)$uri;
+ } else {
+ $uri = $uri->getPath();
+ }
+
+ $loginUrls = (array)$this->getConfig('loginUrl');
+ foreach ($loginUrls as $key => $loginUrl) {
+ if (is_array($loginUrl)) {
+ $loginUrls[$key] = Router::url($loginUrl);
+ }
+ }
+
+ $errors = [
+ sprintf(
+ 'Login URL `%s` did not match `%s`.',
+ $uri,
+ implode('` or `', $loginUrls),
+ ),
+ ];
+
+ return new Result(null, Result::FAILURE_OTHER, $errors);
+ }
+
+ /**
+ * Authenticates the identity contained in a request.
+ *
+ * Will use the `config.userModel`, and `config.fields` to find POST data
+ * that is used to find a matching record in the `config.userModel`.
+ * Will return false if there is no post data, either username or password is missing,
+ * or if the scope conditions have not been met.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ if (!$this->_checkUrl($request)) {
+ return $this->_buildLoginUrlErrorResult($request);
+ }
+
+ $data = $this->_getData($request);
+ if ($data === null) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING, [
+ 'Login credentials not found',
+ ]);
+ }
+
+ $identifier = $this->getIdentifier();
+ $user = $identifier->identify($data);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $identifier->getErrors());
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/HttpBasicAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/HttpBasicAuthenticator.php
new file mode 100644
index 000000000..7d5c2b8d7
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/HttpBasicAuthenticator.php
@@ -0,0 +1,124 @@
+ [
+ AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
+ AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
+ ],
+ 'skipChallenge' => false,
+ ];
+
+ /**
+ * Gets the identifier, loading a default Password identifier if none configured.
+ *
+ * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator().
+ *
+ * @return \Authentication\Identifier\IdentifierInterface
+ */
+ public function getIdentifier(): IdentifierInterface
+ {
+ if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) {
+ $identifierConfig = [];
+ if ($this->getConfig('fields')) {
+ $identifierConfig['fields'] = $this->getConfig('fields');
+ }
+ $this->_identifier->load('Authentication.Password', $identifierConfig);
+ }
+
+ return $this->_identifier;
+ }
+
+ /**
+ * Authenticate a user using HTTP auth. Will use the configured User model and attempt a
+ * login using HTTP auth.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $server = $request->getServerParams();
+ $username = $server['PHP_AUTH_USER'] ?? '';
+ $password = $server['PHP_AUTH_PW'] ?? '';
+
+ if ($username === '' || $password === '') {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING);
+ }
+
+ $user = $this->getIdentifier()->identify([
+ AbstractIdentifier::CREDENTIAL_USERNAME => $username,
+ AbstractIdentifier::CREDENTIAL_PASSWORD => $password,
+ ]);
+
+ if ($user === null) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+
+ /**
+ * Create a challenge exception for basic auth challenge.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request A request object.
+ * @return void
+ * @throws \Authentication\Authenticator\AuthenticationRequiredException
+ */
+ public function unauthorizedChallenge(ServerRequestInterface $request): void
+ {
+ if ($this->getConfig('skipChallenge')) {
+ return;
+ }
+
+ throw new AuthenticationRequiredException($this->loginHeaders($request), '');
+ }
+
+ /**
+ * Generate the login headers
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request Request object.
+ * @return array Headers for logging in.
+ */
+ protected function loginHeaders(ServerRequestInterface $request): array
+ {
+ $server = $request->getServerParams();
+ $realm = $this->getConfig('realm') ?: $server['SERVER_NAME'];
+
+ return ['WWW-Authenticate' => sprintf('Basic realm="%s"', $realm)];
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/HttpDigestAuthenticator.php b/app/vendor/cakephp/authentication/src/Authenticator/HttpDigestAuthenticator.php
new file mode 100644
index 000000000..0647b8eab
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/HttpDigestAuthenticator.php
@@ -0,0 +1,281 @@
+ $config Configuration settings.
+ */
+ public function __construct(IdentifierInterface $identifier, array $config = [])
+ {
+ $secret = '';
+ if (class_exists(Security::class)) {
+ $secret = Security::getSalt();
+ }
+ $this->setConfig([
+ 'realm' => null,
+ 'qop' => 'auth',
+ 'nonceLifetime' => 300,
+ 'opaque' => null,
+ 'secret' => $secret,
+ ]);
+
+ $this->setConfig($config);
+ parent::__construct($identifier, $config);
+
+ $secret = $this->getConfig('secret');
+ if (!is_string($secret) || strlen($secret) === 0) {
+ throw new InvalidArgumentException('Secret key must be a non-empty string.');
+ }
+ }
+
+ /**
+ * Get a user based on information in the request. Used by cookie-less auth for stateless clients.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $digest = $this->_getDigest($request);
+ if ($digest === null) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING);
+ }
+
+ $user = $this->_identifier->identify([
+ AbstractIdentifier::CREDENTIAL_USERNAME => $digest['username'],
+ ]);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
+ }
+
+ if (!$this->validNonce($digest['nonce'])) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_INVALID);
+ }
+
+ $field = $this->_config['fields'][AbstractIdentifier::CREDENTIAL_PASSWORD];
+ $password = $user[$field];
+
+ $server = $request->getServerParams();
+ if (!isset($server['ORIGINAL_REQUEST_METHOD'])) {
+ $server['ORIGINAL_REQUEST_METHOD'] = $server['REQUEST_METHOD'];
+ }
+
+ $hash = $this->generateResponseHash($digest, $password, $server['ORIGINAL_REQUEST_METHOD']);
+ if (hash_equals($hash, $digest['response'])) {
+ return new Result($user, Result::SUCCESS);
+ }
+
+ return new Result(null, Result::FAILURE_CREDENTIALS_INVALID);
+ }
+
+ /**
+ * Gets the digest headers from the request/environment.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return array|null Array of digest information.
+ */
+ protected function _getDigest(ServerRequestInterface $request): ?array
+ {
+ $server = $request->getServerParams();
+ $digest = empty($server['PHP_AUTH_DIGEST']) ? null : $server['PHP_AUTH_DIGEST'];
+ if (!$digest && function_exists('apache_request_headers')) {
+ $headers = apache_request_headers();
+ if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 7) === 'Digest ') {
+ $digest = substr($headers['Authorization'], 7);
+ }
+ }
+ if (!$digest) {
+ return null;
+ }
+
+ return $this->parseAuthData($digest);
+ }
+
+ /**
+ * Parse the digest authentication headers and split them up.
+ *
+ * @param string $digest The raw digest authentication headers.
+ * @return array|null An array of digest authentication headers
+ */
+ public function parseAuthData(string $digest): ?array
+ {
+ if (substr($digest, 0, 7) === 'Digest ') {
+ $digest = substr($digest, 7);
+ }
+ $keys = $match = [];
+ $req = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
+ preg_match_all('/(\w+)=([\'"]?)([a-zA-Z0-9\:\#\%\?\&@=\.\/_-]+)\2/', $digest, $match, PREG_SET_ORDER);
+
+ foreach ($match as $i) {
+ $keys[$i[1]] = $i[3];
+ unset($req[$i[1]]);
+ }
+
+ if (!$req) {
+ return $keys;
+ }
+
+ return null;
+ }
+
+ /**
+ * Generate the response hash for a given digest array.
+ *
+ * @param array $digest Digest information containing data from HttpDigestAuthenticate::parseAuthData().
+ * @param string $password The digest hash password generated with HttpDigestAuthenticate::password()
+ * @param string $method Request method
+ * @return string Response hash
+ */
+ public function generateResponseHash(array $digest, string $password, string $method): string
+ {
+ return md5(
+ $password .
+ ':' . $digest['nonce'] . ':' . $digest['nc'] . ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' .
+ md5($method . ':' . $digest['uri']),
+ );
+ }
+
+ /**
+ * Creates an auth digest password hash to store
+ *
+ * @param string $username The username to use in the digest hash.
+ * @param string $password The unhashed password to make a digest hash for.
+ * @param string $realm The realm the password is for.
+ * @return string the hashed password that can later be used with Digest authentication.
+ */
+ public static function password(string $username, string $password, string $realm): string
+ {
+ return md5($username . ':' . $realm . ':' . $password);
+ }
+
+ /**
+ * Generate the login headers
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return array Headers for logging in.
+ */
+ protected function loginHeaders(ServerRequestInterface $request): array
+ {
+ $server = $request->getServerParams();
+ $realm = $this->_config['realm'] ?: $server['SERVER_NAME'];
+
+ $options = [
+ 'realm' => $realm,
+ 'qop' => $this->_config['qop'],
+ 'nonce' => $this->generateNonce(),
+ 'opaque' => $this->_config['opaque'] ?: md5($realm),
+ ];
+
+ $digest = $this->_getDigest($request);
+ if ($digest !== null && isset($digest['nonce']) && !$this->validNonce($digest['nonce'])) {
+ $options['stale'] = true;
+ }
+
+ $opts = [];
+ foreach ($options as $k => $v) {
+ if (is_bool($v)) {
+ $v = $v ? 'true' : 'false';
+ $opts[] = sprintf('%s=%s', $k, $v);
+ } else {
+ $opts[] = sprintf('%s="%s"', $k, (string)$v);
+ }
+ }
+
+ return ['WWW-Authenticate' => 'Digest ' . implode(',', $opts)];
+ }
+
+ /**
+ * Generate a nonce value that is validated in future requests.
+ *
+ * @return string
+ */
+ protected function generateNonce(): string
+ {
+ $expiryTime = microtime(true) + $this->getConfig('nonceLifetime');
+ $secret = $this->getConfig('secret');
+ $signatureValue = hash_hmac('sha1', $expiryTime . ':' . $secret, $secret);
+ $nonceValue = $expiryTime . ':' . $signatureValue;
+
+ return base64_encode($nonceValue);
+ }
+
+ /**
+ * Check the nonce to ensure it is valid and not expired.
+ *
+ * @param string $nonce The nonce value to check.
+ * @return bool
+ */
+ protected function validNonce(string $nonce): bool
+ {
+ $value = base64_decode($nonce);
+ if ($value === false) {
+ return false;
+ }
+ $parts = explode(':', $value);
+ if (count($parts) !== 2) {
+ return false;
+ }
+ [$expires, $checksum] = $parts;
+ if ($expires < microtime(true)) {
+ return false;
+ }
+ $secret = $this->getConfig('secret');
+ $check = hash_hmac('sha1', $expires . ':' . $secret, $secret);
+
+ return hash_equals($check, $checksum);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/ImpersonationInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/ImpersonationInterface.php
new file mode 100644
index 000000000..fc8201d5f
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/ImpersonationInterface.php
@@ -0,0 +1,57 @@
+ 'Authorization',
+ 'queryParam' => 'token',
+ 'tokenPrefix' => 'bearer',
+ 'algorithm' => 'HS256',
+ 'returnPayload' => true,
+ 'secretKey' => null,
+ 'subjectKey' => JwtSubjectIdentifier::CREDENTIAL_JWT_SUBJECT,
+ 'jwks' => null,
+ ];
+
+ /**
+ * Payload data.
+ *
+ * @var object|null
+ */
+ protected ?object $payload = null;
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct(IdentifierInterface $identifier, array $config = [])
+ {
+ parent::__construct($identifier, $config);
+
+ if (empty($this->_config['secretKey'])) {
+ if (!class_exists(Security::class)) {
+ throw new RuntimeException('You must set the `secretKey` config key for JWT authentication.');
+ }
+ $this->setConfig('secretKey', Security::getSalt());
+ }
+ }
+
+ /**
+ * Gets the identifier, loading a default JwtSubject identifier if none configured.
+ *
+ * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator().
+ *
+ * @return \Authentication\Identifier\IdentifierInterface
+ */
+ public function getIdentifier(): IdentifierInterface
+ {
+ if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) {
+ $this->_identifier->load('Authentication.JwtSubject');
+ }
+
+ return $this->_identifier;
+ }
+
+ /**
+ * Authenticates the identity based on a JWT token contained in a request.
+ *
+ * @link https://jwt.io/
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ try {
+ $result = $this->getPayload($request);
+ } catch (Exception $e) {
+ return new Result(
+ null,
+ Result::FAILURE_CREDENTIALS_INVALID,
+ [
+ 'message' => $e->getMessage(),
+ 'exception' => $e,
+ ],
+ );
+ }
+
+ if (!($result instanceof stdClass)) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_INVALID);
+ }
+
+ /** @phpstan-ignore-next-line */
+ $result = json_decode(json_encode($result), true);
+
+ $subjectKey = $this->getConfig('subjectKey');
+ if (empty($result[$subjectKey])) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING);
+ }
+
+ if ($this->getConfig('returnPayload')) {
+ $user = new ArrayObject($result);
+
+ return new Result($user, Result::SUCCESS);
+ }
+
+ $identifier = $this->getIdentifier();
+ $user = $identifier->identify([
+ $subjectKey => $result[$subjectKey],
+ ]);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $identifier->getErrors());
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+
+ /**
+ * Get payload data.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface|null $request Request to get authentication information from.
+ * @return object|null Payload object on success, null on failure
+ */
+ public function getPayload(?ServerRequestInterface $request = null): ?object
+ {
+ if (!$request) {
+ return $this->payload;
+ }
+
+ $payload = null;
+ $token = $this->getToken($request);
+
+ if ($token !== null) {
+ $payload = $this->decodeToken($token);
+ }
+
+ $this->payload = $payload;
+
+ return $this->payload;
+ }
+
+ /**
+ * Decode JWT token.
+ *
+ * @param string $token JWT token to decode.
+ * @return object|null The JWT's payload as a PHP object, null on failure.
+ */
+ protected function decodeToken(string $token): ?object
+ {
+ $jsonWebKeySet = $this->getConfig('jwks');
+ if ($jsonWebKeySet) {
+ $keySet = JWK::parseKeySet($jsonWebKeySet);
+
+ return JWT::decode(
+ $token,
+ $keySet,
+ );
+ }
+
+ $key = new Key($this->getConfig('secretKey'), $this->getConfig('algorithm'));
+
+ return JWT::decode($token, $key);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/PersistenceInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/PersistenceInterface.php
new file mode 100644
index 000000000..5a925951c
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/PersistenceInterface.php
@@ -0,0 +1,47 @@
+ $config
+ */
+ public function __construct(IdentifierInterface $identifier, array $config = [])
+ {
+ $config += [
+ 'identifierKey' => 'key',
+ 'idField' => 'id',
+ ];
+
+ parent::__construct($identifier, $config);
+ }
+
+ /**
+ * Authenticate a user using session data.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $sessionKey = $this->getConfig('sessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+
+ $userId = $session->read($sessionKey);
+ if (!$userId) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
+ }
+
+ $user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]);
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array
+ {
+ $sessionKey = $this->getConfig('sessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+
+ if (!$session->check($sessionKey)) {
+ $session->renew();
+ $session->write($sessionKey, $identity[$this->getConfig('idField')]);
+ }
+
+ return [
+ 'request' => $request,
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * Impersonates a user
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @param \Psr\Http\Message\ResponseInterface $response The response
+ * @param \ArrayAccess $impersonator User who impersonates
+ * @param \ArrayAccess $impersonated User impersonated
+ * @return array
+ */
+ public function impersonate(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ArrayAccess $impersonator,
+ ArrayAccess $impersonated,
+ ): array {
+ $sessionKey = $this->getConfig('sessionKey');
+ $impersonateSessionKey = $this->getConfig('impersonateSessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+ if ($session->check($impersonateSessionKey)) {
+ throw new UnauthorizedException(
+ 'You are impersonating a user already. ' .
+ 'Stop the current impersonation before impersonating another user.',
+ );
+ }
+ $session->write($impersonateSessionKey, $impersonator[$this->getConfig('idField')]);
+ $session->write($sessionKey, $impersonated[$this->getConfig('idField')]);
+ $this->setConfig('identify', true);
+
+ return [
+ 'request' => $request,
+ 'response' => $response,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/Result.php b/app/vendor/cakephp/authentication/src/Authenticator/Result.php
new file mode 100644
index 000000000..edf7ebff0
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/Result.php
@@ -0,0 +1,110 @@
+_status = $status;
+ $this->_data = $data;
+ $this->_errors = $messages;
+ }
+
+ /**
+ * Returns whether the result represents a successful authentication attempt.
+ *
+ * @return bool
+ */
+ public function isValid(): bool
+ {
+ return $this->_status === ResultInterface::SUCCESS;
+ }
+
+ /**
+ * Get the result status for this authentication attempt.
+ *
+ * @return string
+ */
+ public function getStatus(): string
+ {
+ return $this->_status;
+ }
+
+ /**
+ * Returns the identity data used in the authentication attempt.
+ *
+ * @return \ArrayAccess|array|null
+ */
+ public function getData(): ArrayAccess|array|null
+ {
+ return $this->_data;
+ }
+
+ /**
+ * Returns an array of string reasons why the authentication attempt was unsuccessful.
+ *
+ * If authentication was successful, this method returns an empty array.
+ *
+ * @return array
+ */
+ public function getErrors(): array
+ {
+ return $this->_errors;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/ResultInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/ResultInterface.php
new file mode 100644
index 000000000..565e8bfbc
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/ResultInterface.php
@@ -0,0 +1,77 @@
+ [
+ AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
+ ],
+ 'sessionKey' => 'Auth',
+ 'impersonateSessionKey' => 'AuthImpersonate',
+ 'identify' => false,
+ 'identityAttribute' => 'identity',
+ ];
+
+ /**
+ * Authenticate a user using session data.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $sessionKey = $this->getConfig('sessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+ $user = $session->read($sessionKey);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
+ }
+
+ if ($this->getConfig('identify') === true) {
+ deprecationWarning(
+ '3.4.0',
+ 'The `identify` option is deprecated. ' .
+ 'Use `PrimaryKeySessionAuthenticator` instead to fetch fresh user data on each request.',
+ );
+ $credentials = [];
+ foreach ($this->getConfig('fields') as $key => $field) {
+ $credentials[$key] = $user[$field];
+ }
+ $user = $this->_identifier->identify($credentials);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_INVALID);
+ }
+ }
+
+ if (!($user instanceof ArrayAccess)) {
+ $user = new ArrayObject($user);
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array
+ {
+ $sessionKey = $this->getConfig('sessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+
+ if (!$session->check($sessionKey)) {
+ $session->renew();
+ $session->write($sessionKey, $identity);
+ }
+
+ return [
+ 'request' => $request,
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array
+ {
+ $sessionKey = $this->getConfig('sessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+ $session->delete($sessionKey);
+ $session->renew();
+
+ return [
+ 'request' => $request->withoutAttribute($this->getConfig('identityAttribute')),
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * Impersonates a user
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @param \Psr\Http\Message\ResponseInterface $response The response
+ * @param \ArrayAccess $impersonator User who impersonates
+ * @param \ArrayAccess $impersonated User impersonated
+ * @return array
+ */
+ public function impersonate(
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ ArrayAccess $impersonator,
+ ArrayAccess $impersonated,
+ ): array {
+ $sessionKey = $this->getConfig('sessionKey');
+ $impersonateSessionKey = $this->getConfig('impersonateSessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+ if ($session->check($impersonateSessionKey)) {
+ throw new UnauthorizedException(
+ 'You are impersonating a user already. ' .
+ 'Stop the current impersonation before impersonating another user.',
+ );
+ }
+ $session->write($impersonateSessionKey, $impersonator);
+ $session->write($sessionKey, $impersonated);
+ $this->setConfig('identify', true);
+
+ return [
+ 'request' => $request,
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * Stops impersonation
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @param \Psr\Http\Message\ResponseInterface $response The response
+ * @return array
+ */
+ public function stopImpersonating(ServerRequestInterface $request, ResponseInterface $response): array
+ {
+ $sessionKey = $this->getConfig('sessionKey');
+ $impersonateSessionKey = $this->getConfig('impersonateSessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+ if ($session->check($impersonateSessionKey)) {
+ $identity = $session->read($impersonateSessionKey);
+ $session->delete($impersonateSessionKey);
+ $session->write($sessionKey, $identity);
+ $this->setConfig('identify', true);
+ }
+
+ return [
+ 'request' => $request,
+ 'response' => $response,
+ ];
+ }
+
+ /**
+ * Returns true if impersonation is being done
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request
+ * @return bool
+ */
+ public function isImpersonating(ServerRequestInterface $request): bool
+ {
+ $impersonateSessionKey = $this->getConfig('impersonateSessionKey');
+ /** @var \Cake\Http\Session $session */
+ $session = $request->getAttribute('session');
+
+ return $session->check($impersonateSessionKey);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/StatelessInterface.php b/app/vendor/cakephp/authentication/src/Authenticator/StatelessInterface.php
new file mode 100644
index 000000000..d521b99ec
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/StatelessInterface.php
@@ -0,0 +1,41 @@
+ null,
+ 'queryParam' => null,
+ 'tokenPrefix' => null,
+ ];
+
+ /**
+ * Gets the identifier, loading a default Token identifier if none configured.
+ *
+ * This is done lazily to allow loadIdentifier() to be called after loadAuthenticator().
+ *
+ * @return \Authentication\Identifier\IdentifierInterface
+ */
+ public function getIdentifier(): IdentifierInterface
+ {
+ if ($this->_identifier instanceof IdentifierCollection && $this->_identifier->isEmpty()) {
+ $this->_identifier->load('Authentication.Token');
+ }
+
+ return $this->_identifier;
+ }
+
+ /**
+ * Checks if the token is in the headers or a request parameter
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return string|null
+ */
+ protected function getToken(ServerRequestInterface $request): ?string
+ {
+ $token = $this->getTokenFromHeader($request, $this->getConfig('header'))
+ ?? $this->getTokenFromQuery($request, $this->getConfig('queryParam'));
+
+ $prefix = $this->getConfig('tokenPrefix');
+ if ($prefix !== null && $token !== null) {
+ return $this->stripTokenPrefix($token, $prefix);
+ }
+
+ return $token;
+ }
+
+ /**
+ * Strips a prefix from a token
+ *
+ * @param string $token Token string
+ * @param string $prefix Prefix to strip
+ * @return string
+ */
+ protected function stripTokenPrefix(string $token, string $prefix): string
+ {
+ $prefixLength = mb_strlen($prefix);
+ if (mb_substr(mb_strtolower($token), 0, $prefixLength) === mb_strtolower($prefix)) {
+ $token = mb_substr($token, $prefixLength);
+ }
+
+ return trim($token);
+ }
+
+ /**
+ * Gets the token from the request headers
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @param string|null $headerLine Header name
+ * @return string|null
+ */
+ protected function getTokenFromHeader(ServerRequestInterface $request, ?string $headerLine): ?string
+ {
+ if ($headerLine) {
+ $header = $request->getHeaderLine($headerLine);
+ if ($header) {
+ return $header;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the token from the request query
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @param string|null $queryParam Request query parameter name
+ * @return string|null
+ */
+ protected function getTokenFromQuery(ServerRequestInterface $request, ?string $queryParam): ?string
+ {
+ if (!$queryParam) {
+ return null;
+ }
+
+ $queryParams = $request->getQueryParams();
+
+ return $queryParams[$queryParam] ?? null;
+ }
+
+ /**
+ * Authenticates the identity by token contained in a request.
+ * Token could be passed as query using `config.queryParam` or as header param using `config.header`. Token
+ * prefix will be stripped if `config.tokenPrefix` is set. Will return false if no token is provided or if the
+ * scope conditions have not been met.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request The request that contains login information.
+ * @return \Authentication\Authenticator\ResultInterface
+ */
+ public function authenticate(ServerRequestInterface $request): ResultInterface
+ {
+ $token = $this->getToken($request);
+ if ($token === null) {
+ return new Result(null, Result::FAILURE_CREDENTIALS_MISSING);
+ }
+
+ $identifier = $this->getIdentifier();
+ $user = $identifier->identify([
+ TokenIdentifier::CREDENTIAL_TOKEN => $token,
+ ]);
+
+ if (!$user) {
+ return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND, $identifier->getErrors());
+ }
+
+ return new Result($user, Result::SUCCESS);
+ }
+
+ /**
+ * No-op method.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request A request object.
+ * @return void
+ */
+ public function unauthorizedChallenge(ServerRequestInterface $request): void
+ {
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Authenticator/UnauthenticatedException.php b/app/vendor/cakephp/authentication/src/Authenticator/UnauthenticatedException.php
new file mode 100644
index 000000000..538129d8b
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Authenticator/UnauthenticatedException.php
@@ -0,0 +1,46 @@
+
+ */
+class AuthenticationComponent extends Component implements EventDispatcherInterface
+{
+ /**
+ * @use \Cake\Event\EventDispatcherTrait<\Cake\Controller\Controller>
+ */
+ use EventDispatcherTrait;
+
+ /**
+ * Configuration options
+ *
+ * - `logoutRedirect` - The route/URL to direct users to after logout()
+ * - `requireIdentity` - By default AuthenticationComponent will require an
+ * identity to be present whenever it is active. You can set the option to
+ * false to disable that behavior. See allowUnauthenticated() as well.
+ * - `unauthenticatedMessage` - Error message to use when `UnauthenticatedException` is thrown.
+ *
+ * @var array
+ */
+ protected array $_defaultConfig = [
+ 'logoutRedirect' => false,
+ 'requireIdentity' => true,
+ 'identityAttribute' => 'identity',
+ 'identityCheckEvent' => 'Controller.startup',
+ 'unauthenticatedMessage' => null,
+ ];
+
+ /**
+ * List of actions that don't require authentication.
+ *
+ * @var array
+ */
+ protected array $unauthenticatedActions = [];
+
+ /**
+ * Authentication service instance.
+ *
+ * @var \Authentication\AuthenticationServiceInterface|null
+ */
+ protected ?AuthenticationServiceInterface $_authentication = null;
+
+ /**
+ * Initialize component.
+ *
+ * @param array $config The config data.
+ * @return void
+ */
+ public function initialize(array $config): void
+ {
+ $controller = $this->getController();
+ $this->setEventManager($controller->getEventManager());
+ }
+
+ /**
+ * Triggers the Authentication.afterIdentify event for non stateless adapters that are not persistent either
+ *
+ * @return void
+ */
+ public function beforeFilter(): void
+ {
+ $authentication = $this->getAuthenticationService();
+ $provider = $authentication->getAuthenticationProvider();
+
+ if (
+ $provider !== null &&
+ !$provider instanceof PersistenceInterface &&
+ !$provider instanceof StatelessInterface
+ ) {
+ $this->dispatchEvent('Authentication.afterIdentify', [
+ 'provider' => $provider,
+ 'identity' => $this->getIdentity(),
+ 'service' => $authentication,
+ ], $this->getController());
+ }
+
+ if ($this->getConfig('identityCheckEvent') === 'Controller.initialize') {
+ $this->doIdentityCheck();
+ }
+ }
+
+ /**
+ * Start up event handler
+ *
+ * @return void
+ * @throws \Exception when request is missing or has an invalid AuthenticationService
+ * @throws \Authentication\Authenticator\UnauthenticatedException when requireIdentity is true and request is missing an identity
+ */
+ public function startup(): void
+ {
+ if ($this->getConfig('identityCheckEvent') === 'Controller.startup') {
+ $this->doIdentityCheck();
+ }
+ }
+
+ /**
+ * Returns authentication service.
+ *
+ * @return \Authentication\AuthenticationServiceInterface
+ * @throws \Exception
+ */
+ public function getAuthenticationService(): AuthenticationServiceInterface
+ {
+ if ($this->_authentication !== null) {
+ return $this->_authentication;
+ }
+
+ $controller = $this->getController();
+ $service = $controller->getRequest()->getAttribute('authentication');
+ if ($service === null) {
+ throw new Exception(
+ 'The request object does not contain the required `authentication` attribute. Verify the ' .
+ 'AuthenticationMiddleware has been added.',
+ );
+ }
+
+ if (!($service instanceof AuthenticationServiceInterface)) {
+ throw new Exception('Authentication service does not implement ' . AuthenticationServiceInterface::class);
+ }
+
+ $this->_authentication = $service;
+
+ return $service;
+ }
+
+ /**
+ * Check if the identity presence is required.
+ *
+ * Also checks if the current action is accessible without authentication.
+ *
+ * @return void
+ * @throws \Exception when request is missing or has an invalid AuthenticationService
+ * @throws \Authentication\Authenticator\UnauthenticatedException when requireIdentity is true and request is missing an identity
+ */
+ protected function doIdentityCheck(): void
+ {
+ if (!$this->getConfig('requireIdentity')) {
+ return;
+ }
+
+ $request = $this->getController()->getRequest();
+ $action = $request->getParam('action');
+ if (in_array($action, $this->unauthenticatedActions, true)) {
+ return;
+ }
+
+ $identity = $request->getAttribute($this->getConfig('identityAttribute'));
+ if (!$identity) {
+ throw new UnauthenticatedException($this->getConfig('unauthenticatedMessage', ''));
+ }
+ }
+
+ /**
+ * Disables the identity check for this controller and as all its actions.
+ * It then doesn't require an authentication identity to be present.
+ *
+ * @return void
+ */
+ public function disableIdentityCheck(): void
+ {
+ $this->setConfig('requireIdentity', false);
+ }
+
+ /**
+ * Set the list of actions that don't require an authentication identity to be present.
+ *
+ * Actions not in this list will require an identity to be present. Any
+ * valid identity will pass this constraint.
+ *
+ * @param array $actions The action list.
+ * @return $this
+ */
+ public function allowUnauthenticated(array $actions)
+ {
+ $this->unauthenticatedActions = $actions;
+
+ return $this;
+ }
+
+ /**
+ * Add to the list of actions that don't require an authentication identity to be present.
+ *
+ * @param array $actions The action or actions to append.
+ * @return $this
+ */
+ public function addUnauthenticatedActions(array $actions)
+ {
+ $this->unauthenticatedActions = array_merge($this->unauthenticatedActions, $actions);
+ $this->unauthenticatedActions = array_values(array_unique($this->unauthenticatedActions));
+
+ return $this;
+ }
+
+ /**
+ * Get the current list of actions that don't require authentication.
+ *
+ * @return array
+ */
+ public function getUnauthenticatedActions(): array
+ {
+ return $this->unauthenticatedActions;
+ }
+
+ /**
+ * Gets the result of the last authenticate() call.
+ *
+ * @return \Authentication\Authenticator\ResultInterface|null Authentication result interface
+ */
+ public function getResult(): ?ResultInterface
+ {
+ return $this->getAuthenticationService()->getResult();
+ }
+
+ /**
+ * Get the identifier (primary key) of the identity.
+ *
+ * @return array|string|int|null
+ */
+ public function getIdentifier(): array|string|int|null
+ {
+ return $this->getIdentity()?->getIdentifier();
+ }
+
+ /**
+ * Returns the identity used in the authentication attempt.
+ *
+ * @return \Authentication\IdentityInterface|null
+ */
+ public function getIdentity(): ?IdentityInterface
+ {
+ $controller = $this->getController();
+ $identity = $controller->getRequest()->getAttribute($this->getConfig('identityAttribute'));
+
+ return $identity;
+ }
+
+ /**
+ * Returns the identity used in the authentication attempt.
+ *
+ * @param string $path Path to return from the data.
+ * @return mixed
+ * @throws \RuntimeException If the identity has not been found.
+ */
+ public function getIdentityData(string $path): mixed
+ {
+ $identity = $this->getIdentity();
+
+ if ($identity === null) {
+ throw new RuntimeException('The identity has not been found.');
+ }
+
+ return Hash::get($identity, $path);
+ }
+
+ /**
+ * Replace the current identity
+ *
+ * Clear and replace identity data in all authenticators
+ * that are loaded and support persistence. The identity
+ * is cleared and then set to ensure that privilege escalation
+ * and de-escalation include side effects like session rotation.
+ *
+ * @param \ArrayAccess|array $identity Identity data to persist.
+ * @return $this
+ */
+ public function setIdentity(ArrayAccess|array $identity)
+ {
+ $controller = $this->getController();
+ $service = $this->getAuthenticationService();
+
+ $service->clearIdentity($controller->getRequest(), $controller->getResponse());
+
+ /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */
+ $result = $service->persistIdentity(
+ $controller->getRequest(),
+ $controller->getResponse(),
+ $identity,
+ );
+
+ $controller->setRequest($result['request']);
+ $controller->setResponse($result['response']);
+
+ return $this;
+ }
+
+ /**
+ * Log a user out.
+ *
+ * Triggers the `Authentication.logout` event.
+ *
+ * @return string|null Returns null or `logoutRedirect`.
+ */
+ public function logout(): ?string
+ {
+ $controller = $this->getController();
+ /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */
+ $result = $this->getAuthenticationService()->clearIdentity(
+ $controller->getRequest(),
+ $controller->getResponse(),
+ );
+
+ $controller->setRequest($result['request']);
+ $controller->setResponse($result['response']);
+
+ $this->dispatchEvent('Authentication.logout', [], $controller);
+
+ $logoutRedirect = $this->getConfig('logoutRedirect');
+ if ($logoutRedirect === false) {
+ return null;
+ }
+
+ return Router::normalize($logoutRedirect);
+ }
+
+ /**
+ * Get the URL visited before an unauthenticated redirect.
+ *
+ * Reads from the current request's query string if available.
+ *
+ * Leverages the `unauthenticatedRedirect` and `queryParam` options in
+ * the AuthenticationService.
+ *
+ * @param array|string|null $default Default URL to use if no redirect URL is available.
+ * @return string|null
+ */
+ public function getLoginRedirect(array|string|null $default = null): ?string
+ {
+ if (is_array($default)) {
+ $default = Router::url(['_base' => false] + $default);
+ }
+
+ return $this->getAuthenticationService()->getLoginRedirect($this->getController()->getRequest()) ?? $default;
+ }
+
+ /**
+ * Get the Controller callbacks this Component is interested in.
+ *
+ * @return array
+ */
+ public function implementedEvents(): array
+ {
+ return [
+ 'Controller.initialize' => 'beforeFilter',
+ 'Controller.startup' => 'startup',
+ ];
+ }
+
+ /**
+ * Impersonates a user
+ *
+ * @param \ArrayAccess $impersonated User impersonated
+ * @return $this
+ * @throws \Exception
+ * @see https://book.cakephp.org/authentication/3/en/impersonation.html
+ */
+ public function impersonate(ArrayAccess $impersonated)
+ {
+ $service = $this->getImpersonationAuthenticationService();
+
+ $identity = $this->getIdentity();
+ if (!$identity) {
+ throw new UnauthenticatedException('You must be logged in before impersonating a user.');
+ }
+ $impersonator = $identity->getOriginalData();
+ if (!($impersonator instanceof ArrayAccess)) {
+ $impersonator = new ArrayObject($impersonator);
+ }
+ $controller = $this->getController();
+ /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */
+ $result = $service->impersonate(
+ $controller->getRequest(),
+ $controller->getResponse(),
+ $impersonator,
+ $impersonated,
+ );
+
+ if (!$service->isImpersonating($controller->getRequest())) {
+ throw new UnexpectedValueException('An error has occurred impersonating user.');
+ }
+
+ $controller->setRequest($result['request']);
+ $controller->setResponse($result['response']);
+
+ return $this;
+ }
+
+ /**
+ * Stops impersonation
+ *
+ * @return $this
+ * @throws \Exception
+ * @see https://book.cakephp.org/authentication/3/en/impersonation.html
+ */
+ public function stopImpersonating()
+ {
+ $service = $this->getImpersonationAuthenticationService();
+
+ $controller = $this->getController();
+
+ /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */
+ $result = $service->stopImpersonating(
+ $controller->getRequest(),
+ $controller->getResponse(),
+ );
+
+ if ($service->isImpersonating($controller->getRequest())) {
+ throw new UnexpectedValueException('An error has occurred stopping impersonation.');
+ }
+
+ $controller->setRequest($result['request']);
+ $controller->setResponse($result['response']);
+
+ return $this;
+ }
+
+ /**
+ * Returns true if impersonation is being done
+ *
+ * @return bool
+ * @throws \Exception
+ * @see https://book.cakephp.org/authentication/3/en/impersonation.html
+ */
+ public function isImpersonating(): bool
+ {
+ if (!$this->getIdentity()) {
+ return false;
+ }
+
+ $service = $this->getImpersonationAuthenticationService();
+ $controller = $this->getController();
+
+ return $service->isImpersonating(
+ $controller->getRequest(),
+ );
+ }
+
+ /**
+ * Get impersonation authentication service
+ *
+ * @return \Authentication\Authenticator\ImpersonationInterface
+ * @throws \Exception
+ * @see https://book.cakephp.org/authentication/3/en/impersonation.html
+ */
+ protected function getImpersonationAuthenticationService(): ImpersonationInterface
+ {
+ $service = $this->getAuthenticationService();
+ if (!($service instanceof ImpersonationInterface)) {
+ $className = get_class($service);
+ throw new InvalidArgumentException(
+ "The {$className} must implement ImpersonationInterface in order to use impersonation.",
+ );
+ }
+
+ return $service;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/AbstractIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/AbstractIdentifier.php
new file mode 100644
index 000000000..e2e36e368
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/AbstractIdentifier.php
@@ -0,0 +1,62 @@
+ $config Configuration
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+ }
+
+ /**
+ * Returns errors
+ *
+ * @return array
+ */
+ public function getErrors(): array
+ {
+ return $this->_errors;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/CallbackIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/CallbackIdentifier.php
new file mode 100644
index 000000000..6b1509d9a
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/CallbackIdentifier.php
@@ -0,0 +1,89 @@
+ null,
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct(array $config)
+ {
+ parent::__construct($config);
+
+ $this->checkCallable();
+ }
+
+ /**
+ * Check the callable option
+ *
+ * @throws \InvalidArgumentException
+ * @return void
+ */
+ protected function checkCallable(): void
+ {
+ $callback = $this->getConfig('callback');
+
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException(sprintf(
+ 'The `callback` option is not a callable. Got `%s` instead.',
+ gettype($callback),
+ ));
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function identify(array $credentials): ArrayAccess|array|null
+ {
+ $callback = $this->getConfig('callback');
+
+ $result = $callback($credentials);
+ if ($result instanceof Result) {
+ $this->_errors = $result->getErrors();
+
+ return $result->getData();
+ }
+ if ($result === null || $result instanceof ArrayAccess) {
+ return $result;
+ }
+
+ throw new RuntimeException(sprintf(
+ 'Invalid return type of `%s`. Expecting `%s` or `null`.',
+ gettype($result),
+ ArrayAccess::class,
+ ));
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/IdentifierCollection.php b/app/vendor/cakephp/authentication/src/Identifier/IdentifierCollection.php
new file mode 100644
index 000000000..257b25d8e
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/IdentifierCollection.php
@@ -0,0 +1,132 @@
+
+ */
+class IdentifierCollection extends AbstractCollection implements IdentifierInterface
+{
+ /**
+ * Errors
+ *
+ * @var array
+ */
+ protected array $_errors = [];
+
+ /**
+ * Identifier that successfully Identified the identity.
+ *
+ * @var \Authentication\Identifier\IdentifierInterface|null
+ */
+ protected ?IdentifierInterface $_successfulIdentifier = null;
+
+ /**
+ * Identifies an user or service by the passed credentials
+ *
+ * @param array $credentials Authentication credentials
+ * @return \ArrayAccess|array|null
+ */
+ public function identify(array $credentials): ArrayAccess|array|null
+ {
+ /** @var \Authentication\Identifier\IdentifierInterface $identifier */
+ foreach ($this->_loaded as $name => $identifier) {
+ $result = $identifier->identify($credentials);
+ if ($result) {
+ $this->_successfulIdentifier = $identifier;
+
+ return $result;
+ }
+
+ $errors = $identifier->getErrors();
+ if ($errors) {
+ $this->_errors[$name] = $identifier->getErrors();
+ }
+ }
+
+ $this->_successfulIdentifier = null;
+
+ return null;
+ }
+
+ /**
+ * Creates identifier instance.
+ *
+ * @param \Authentication\Identifier\IdentifierInterface|class-string<\Authentication\Identifier\IdentifierInterface> $class Identifier class.
+ * @param string $alias Identifier alias.
+ * @param array $config Config array.
+ * @return \Authentication\Identifier\IdentifierInterface
+ * @throws \RuntimeException
+ */
+ protected function _create(object|string $class, string $alias, array $config): IdentifierInterface
+ {
+ if (is_object($class)) {
+ return $class;
+ }
+
+ return new $class($config);
+ }
+
+ /**
+ * Get errors
+ *
+ * @return array
+ */
+ public function getErrors(): array
+ {
+ return $this->_errors;
+ }
+
+ /**
+ * Resolves identifier class name.
+ *
+ * @param string $class Class name to be resolved.
+ * @return class-string<\Authentication\Identifier\IdentifierInterface>|null
+ */
+ protected function _resolveClassName(string $class): ?string
+ {
+ /** @var class-string<\Authentication\Identifier\IdentifierInterface>|null */
+ return App::className($class, 'Identifier', 'Identifier');
+ }
+
+ /**
+ * @param string $class Missing class.
+ * @param string $plugin Class plugin.
+ * @return void
+ * @throws \RuntimeException
+ */
+ protected function _throwMissingClassError(string $class, ?string $plugin): void
+ {
+ $message = sprintf('Identifier class `%s` was not found.', $class);
+ throw new RuntimeException($message);
+ }
+
+ /**
+ * Gets the successful identifier instance if one was successful after calling identify.
+ *
+ * @return \Authentication\Identifier\IdentifierInterface|null
+ */
+ public function getIdentificationProvider(): ?IdentifierInterface
+ {
+ return $this->_successfulIdentifier;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/IdentifierInterface.php b/app/vendor/cakephp/authentication/src/Identifier/IdentifierInterface.php
new file mode 100644
index 000000000..adf29c22b
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/IdentifierInterface.php
@@ -0,0 +1,37 @@
+ 'id',
+ 'dataField' => self::CREDENTIAL_JWT_SUBJECT,
+ 'resolver' => 'Authentication.Orm',
+ ];
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/Ldap/AdapterInterface.php b/app/vendor/cakephp/authentication/src/Identifier/Ldap/AdapterInterface.php
new file mode 100644
index 000000000..2ad03810d
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/Ldap/AdapterInterface.php
@@ -0,0 +1,53 @@
+_setErrorHandler();
+ $result = ldap_bind($this->getConnection(), $bind, $password);
+ $this->_unsetErrorHandler();
+
+ return $result;
+ }
+
+ /**
+ * Get the LDAP connection
+ *
+ * @return \LDAP\Connection
+ * @throws \RuntimeException If the connection is empty
+ */
+ public function getConnection(): Connection
+ {
+ if ($this->_connection === null) {
+ throw new RuntimeException('You are not connected to a LDAP server.');
+ }
+
+ return $this->_connection;
+ }
+
+ /**
+ * Connect to an LDAP server
+ *
+ * @param string $host Hostname
+ * @param int $port Port
+ * @param array $options Additional LDAP options
+ * @return void
+ */
+ public function connect(string $host, int $port, array $options): void
+ {
+ $this->_setErrorHandler();
+ $resource = ldap_connect("{$host}:{$port}");
+ if ($resource === false) {
+ throw new RuntimeException('Unable to connect to LDAP server.');
+ }
+ if (isset($options['tls']) && $options['tls']) {
+ //convert the connection to TLS
+ if (!ldap_start_tls($resource)) {
+ throw new RuntimeException('Starting TLS failed on connection to LDAP server.');
+ }
+ }
+ unset($options['tls']); //don't pass through to PHP LDAP functions
+ $this->_connection = $resource;
+ $this->_unsetErrorHandler();
+
+ foreach ($options as $option => $value) {
+ $this->setOption((int)$option, $value);
+ }
+ }
+
+ /**
+ * Set the value of the given option
+ *
+ * @param int $option Option to set
+ * @param mixed $value The new value for the specified option
+ * @return void
+ */
+ public function setOption(int $option, mixed $value): void
+ {
+ $this->_setErrorHandler();
+ ldap_set_option($this->getConnection(), $option, $value);
+ $this->_unsetErrorHandler();
+ }
+
+ /**
+ * Get the current value for given option
+ *
+ * @param int $option Option to get
+ * @return mixed This will be set to the option value.
+ */
+ public function getOption(int $option): mixed
+ {
+ $this->_setErrorHandler();
+ ldap_get_option($this->getConnection(), $option, $returnValue);
+ $this->_unsetErrorHandler();
+
+ return $returnValue;
+ }
+
+ /**
+ * Get the diagnostic message
+ *
+ * @return string|null
+ */
+ public function getDiagnosticMessage(): ?string
+ {
+ return $this->getOption(LDAP_OPT_DIAGNOSTIC_MESSAGE);
+ }
+
+ /**
+ * Unbind from LDAP directory
+ *
+ * @return void
+ */
+ public function unbind(): void
+ {
+ if ($this->_connection === null) {
+ return;
+ }
+
+ $this->_setErrorHandler();
+ /** @phpstan-ignore-next-line */
+ ldap_unbind($this->_connection);
+ $this->_unsetErrorHandler();
+
+ $this->_connection = null;
+ }
+
+ /**
+ * Set an error handler to turn LDAP errors into exceptions
+ *
+ * @return void
+ * @throws \ErrorException
+ */
+ protected function _setErrorHandler(): void
+ {
+ set_error_handler(
+ function ($errorNumber, $errorText): void {
+ throw new ErrorException($errorText);
+ },
+ E_ALL,
+ );
+ }
+
+ /**
+ * Restore the error handler
+ *
+ * @return void
+ */
+ protected function _unsetErrorHandler(): void
+ {
+ restore_error_handler();
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/LdapIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/LdapIdentifier.php
new file mode 100644
index 000000000..703981498
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/LdapIdentifier.php
@@ -0,0 +1,230 @@
+ 'ldap.example.com',
+ * 'bindDN' => function($username) {
+ * return $username; //transform into a rdn or dn
+ * },
+ * 'options' => [
+ * LDAP_OPT_PROTOCOL_VERSION => 3
+ * ]
+ * ]);
+ * ```
+ *
+ * @link https://github.com/QueenCityCodeFactory/LDAP
+ */
+class LdapIdentifier extends AbstractIdentifier
+{
+ /**
+ * Default configuration
+ *
+ * @var array
+ */
+ protected array $_defaultConfig = [
+ 'ldap' => ExtensionAdapter::class,
+ 'fields' => [
+ self::CREDENTIAL_USERNAME => 'username',
+ self::CREDENTIAL_PASSWORD => 'password',
+ ],
+ 'port' => 389,
+ 'options' => [
+ 'tls' => false,
+ ],
+ ];
+
+ /**
+ * List of errors
+ *
+ * @var array
+ */
+ protected array $_errors = [];
+
+ /**
+ * LDAP connection object
+ *
+ * @var \Authentication\Identifier\Ldap\AdapterInterface
+ */
+ protected AdapterInterface $_ldap;
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct(array $config = [])
+ {
+ parent::__construct($config);
+
+ $this->_checkLdapConfig();
+ $this->_buildLdapObject();
+ }
+
+ /**
+ * Checks the LDAP config
+ *
+ * @throws \RuntimeException
+ * @throws \InvalidArgumentException
+ * @return void
+ */
+ protected function _checkLdapConfig(): void
+ {
+ if (!isset($this->_config['bindDN'])) {
+ throw new RuntimeException('Config `bindDN` is not set.');
+ }
+ if (!is_callable($this->_config['bindDN'])) {
+ throw new InvalidArgumentException(sprintf(
+ 'The `bindDN` config is not a callable. Got `%s` instead.',
+ gettype($this->_config['bindDN']),
+ ));
+ }
+ if (!isset($this->_config['host'])) {
+ throw new RuntimeException('Config `host` is not set.');
+ }
+ }
+
+ /**
+ * Constructs the LDAP object and sets it to the property
+ *
+ * @throws \RuntimeException
+ * @return void
+ */
+ protected function _buildLdapObject(): void
+ {
+ $ldap = $this->_config['ldap'];
+
+ if (is_string($ldap)) {
+ $class = App::className($ldap, 'Identifier/Ldap');
+ if (!$class) {
+ throw new RuntimeException(sprintf(
+ 'Could not find LDAP identfier named `%s`',
+ $ldap,
+ ));
+ }
+ $ldap = new $class();
+ }
+
+ if (!($ldap instanceof AdapterInterface)) {
+ $message = sprintf('Option `ldap` must implement `%s`.', AdapterInterface::class);
+ throw new RuntimeException($message);
+ }
+
+ $this->_ldap = $ldap;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function identify(array $credentials): ArrayAccess|array|null
+ {
+ $this->_connectLdap();
+ $fields = $this->getConfig('fields');
+
+ $isUsernameSet = isset($credentials[$fields[self::CREDENTIAL_USERNAME]]);
+ $isPasswordSet = isset($credentials[$fields[self::CREDENTIAL_PASSWORD]]);
+ if ($isUsernameSet && $isPasswordSet) {
+ return $this->_bindUser(
+ $credentials[$fields[self::CREDENTIAL_USERNAME]],
+ $credentials[$fields[self::CREDENTIAL_PASSWORD]],
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns configured LDAP adapter.
+ *
+ * @return \Authentication\Identifier\Ldap\AdapterInterface
+ */
+ public function getAdapter(): AdapterInterface
+ {
+ return $this->_ldap;
+ }
+
+ /**
+ * Initializes the LDAP connection
+ *
+ * @return void
+ */
+ protected function _connectLdap(): void
+ {
+ $config = $this->getConfig();
+
+ $this->_ldap->connect(
+ $config['host'],
+ $config['port'],
+ (array)$this->getConfig('options'),
+ );
+ }
+
+ /**
+ * Try to bind the given user to the LDAP server
+ *
+ * @param string $username The username
+ * @param string $password The password
+ * @return \ArrayAccess|null
+ */
+ protected function _bindUser(string $username, string $password): ?ArrayAccess
+ {
+ $config = $this->getConfig();
+ try {
+ $ldapBind = $this->_ldap->bind($config['bindDN']($username), $password);
+ if ($ldapBind === true) {
+ $this->_ldap->unbind();
+
+ return new ArrayObject([
+ $config['fields'][self::CREDENTIAL_USERNAME] => $username,
+ ]);
+ }
+ } catch (ErrorException $e) {
+ $this->_handleLdapError($e->getMessage());
+ }
+ $this->_ldap->unbind();
+
+ return null;
+ }
+
+ /**
+ * Handles an LDAP error
+ *
+ * @param string $message Exception message
+ * @return void
+ */
+ protected function _handleLdapError(string $message): void
+ {
+ $extendedError = $this->_ldap->getDiagnosticMessage();
+ if (!is_null($extendedError)) {
+ $this->_errors[] = $extendedError;
+ }
+ $this->_errors[] = $message;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/PasswordIdentifier.php b/app/vendor/cakephp/authentication/src/Identifier/PasswordIdentifier.php
new file mode 100644
index 000000000..45c39353e
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/PasswordIdentifier.php
@@ -0,0 +1,160 @@
+ [
+ * 'username' => ['username', 'email'],
+ * 'password' => 'password'
+ * ]
+ * ]);
+ * ```
+ *
+ * When configuring PasswordIdentifier you can pass in config to which fields,
+ * model and additional conditions are used.
+ */
+class PasswordIdentifier extends AbstractIdentifier
+{
+ use PasswordHasherTrait {
+ getPasswordHasher as protected _getPasswordHasher;
+ }
+ use ResolverAwareTrait;
+
+ /**
+ * Default configuration.
+ * - `fields` The fields to use to identify a user by:
+ * - `username`: one or many username fields.
+ * - `password`: password field.
+ * - `resolver` The resolver implementation to use.
+ * - `passwordHasher` Password hasher class. Can be a string specifying class name
+ * or an array containing `className` key, any other keys will be passed as
+ * config to the class. Defaults to 'Default'.
+ *
+ * @var array
+ */
+ protected array $_defaultConfig = [
+ 'fields' => [
+ self::CREDENTIAL_USERNAME => 'username',
+ self::CREDENTIAL_PASSWORD => 'password',
+ ],
+ 'resolver' => 'Authentication.Orm',
+ 'passwordHasher' => null,
+ ];
+
+ /**
+ * Return password hasher object.
+ *
+ * @return \Authentication\PasswordHasher\PasswordHasherInterface Password hasher instance.
+ */
+ public function getPasswordHasher(): PasswordHasherInterface
+ {
+ if ($this->_passwordHasher === null) {
+ $passwordHasher = $this->getConfig('passwordHasher');
+ if ($passwordHasher !== null) {
+ $passwordHasher = PasswordHasherFactory::build($passwordHasher);
+ } else {
+ $passwordHasher = $this->_getPasswordHasher();
+ }
+ $this->_passwordHasher = $passwordHasher;
+ }
+
+ return $this->_passwordHasher;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function identify(array $credentials): ArrayAccess|array|null
+ {
+ if (!isset($credentials[self::CREDENTIAL_USERNAME])) {
+ return null;
+ }
+
+ $identity = $this->_findIdentity($credentials[self::CREDENTIAL_USERNAME]);
+ if (array_key_exists(self::CREDENTIAL_PASSWORD, $credentials)) {
+ $password = $credentials[self::CREDENTIAL_PASSWORD];
+ if (!$this->_checkPassword($identity, $password)) {
+ return null;
+ }
+ }
+
+ return $identity;
+ }
+
+ /**
+ * Find a user record using the username and password provided.
+ * Input passwords will be hashed even when a user doesn't exist. This
+ * helps mitigate timing attacks that are attempting to find valid usernames.
+ *
+ * @param \ArrayAccess|array|null $identity The identity or null.
+ * @param string|null $password The password.
+ * @return bool
+ */
+ protected function _checkPassword(ArrayAccess|array|null $identity, ?string $password): bool
+ {
+ $passwordField = $this->getConfig('fields.' . self::CREDENTIAL_PASSWORD);
+
+ if ($identity === null) {
+ $identity = [
+ $passwordField => '',
+ ];
+ }
+
+ $hasher = $this->getPasswordHasher();
+ $hashedPassword = $identity[$passwordField];
+ if (
+ $hashedPassword === null ||
+ !$hasher->check((string)$password, $hashedPassword)
+ ) {
+ return false;
+ }
+
+ $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);
+
+ return true;
+ }
+
+ /**
+ * Find a user record using the username/identifier provided.
+ *
+ * @param string $identifier The username/identifier.
+ * @return \ArrayAccess|array|null
+ */
+ protected function _findIdentity(string $identifier): ArrayAccess|array|null
+ {
+ $fields = $this->getConfig('fields.' . self::CREDENTIAL_USERNAME);
+ $conditions = [];
+ foreach ((array)$fields as $field) {
+ $conditions[$field] = $identifier;
+ }
+
+ return $this->getResolver()->find($conditions, ResolverInterface::TYPE_OR);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/Resolver/OrmResolver.php b/app/vendor/cakephp/authentication/src/Identifier/Resolver/OrmResolver.php
new file mode 100644
index 000000000..071ff9d50
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/Resolver/OrmResolver.php
@@ -0,0 +1,81 @@
+ ['some_finder_option' => 'some_value']]
+ *
+ * @var array
+ */
+ protected array $_defaultConfig = [
+ 'userModel' => 'Users',
+ 'finder' => 'all',
+ ];
+
+ /**
+ * Constructor.
+ *
+ * @param array $config Config array.
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function find(array $conditions, string $type = self::TYPE_AND): ArrayAccess|array|null
+ {
+ $table = $this->getTableLocator()->get($this->_config['userModel']);
+
+ $query = $table->selectQuery();
+ $finders = (array)$this->_config['finder'];
+ foreach ($finders as $finder => $options) {
+ if (is_string($options)) {
+ $query->find($options);
+ } else {
+ $query->find($finder, ...$options);
+ }
+ }
+
+ $where = [];
+ foreach ($conditions as $field => $value) {
+ $field = $table->aliasField($field);
+ if (is_array($value)) {
+ $field = $field . ' IN';
+ }
+ $where[$field] = $value;
+ }
+
+ return $query->where([$type => $where])->first();
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverAwareTrait.php b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverAwareTrait.php
new file mode 100644
index 000000000..7997d7bf9
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverAwareTrait.php
@@ -0,0 +1,99 @@
+resolver === null) {
+ $config = $this->getConfig('resolver');
+ if ($config !== null) {
+ $this->resolver = $this->buildResolver($config);
+ } else {
+ throw new RuntimeException('Resolver has not been set.');
+ }
+ }
+
+ return $this->resolver;
+ }
+
+ /**
+ * Sets ResolverInterface instance.
+ *
+ * @param \Authentication\Identifier\Resolver\ResolverInterface $resolver Resolver instance.
+ * @return $this
+ */
+ public function setResolver(ResolverInterface $resolver)
+ {
+ $this->resolver = $resolver;
+
+ return $this;
+ }
+
+ /**
+ * Builds a ResolverInterface instance.
+ *
+ * @param array|string $config Resolver class name or config.
+ * @return \Authentication\Identifier\Resolver\ResolverInterface
+ * @throws \InvalidArgumentException When className option is missing or class name does not exist.
+ * @throws \RuntimeException When resolver does not implement ResolverInterface.
+ */
+ protected function buildResolver(array|string $config): ResolverInterface
+ {
+ if (is_string($config)) {
+ $config = [
+ 'className' => $config,
+ ];
+ }
+ if (!isset($config['className'])) {
+ $message = 'Option `className` is not present.';
+ throw new InvalidArgumentException($message);
+ }
+
+ $class = App::className($config['className'], 'Identifier/Resolver', 'Resolver');
+ if ($class === null) {
+ $message = sprintf('Resolver class `%s` does not exist.', $config['className']);
+ throw new InvalidArgumentException($message);
+ }
+ $instance = new $class($config);
+
+ if (!($instance instanceof ResolverInterface)) {
+ $message = sprintf('Resolver must implement `%s`.', ResolverInterface::class);
+ throw new RuntimeException($message);
+ }
+
+ return $instance;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverInterface.php b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverInterface.php
new file mode 100644
index 000000000..20fb1f2b8
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identifier/Resolver/ResolverInterface.php
@@ -0,0 +1,34 @@
+ 'token',
+ 'dataField' => self::CREDENTIAL_TOKEN,
+ 'resolver' => 'Authentication.Orm',
+ 'hashAlgorithm' => null,
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function identify(array $credentials): ArrayAccess|array|null
+ {
+ $dataField = $this->getConfig('dataField');
+ if (!isset($credentials[$dataField])) {
+ return null;
+ }
+
+ if ($this->getConfig('hashAlgorithm') !== null) {
+ $credentials[$dataField] = Security::hash(
+ $credentials[$dataField],
+ $this->getConfig('hashAlgorithm'),
+ );
+ }
+
+ $conditions = [
+ $this->getConfig('tokenField') => $credentials[$dataField],
+ ];
+
+ return $this->getResolver()->find($conditions);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Identity.php b/app/vendor/cakephp/authentication/src/Identity.php
new file mode 100644
index 000000000..0a699fce9
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Identity.php
@@ -0,0 +1,183 @@
+ [
+ 'id' => 'id',
+ ],
+ ];
+
+ /**
+ * Identity data
+ *
+ * @var \ArrayAccess|array
+ */
+ protected ArrayAccess|array $data;
+
+ /**
+ * Constructor
+ *
+ * @param \ArrayAccess|array $data Identity data
+ * @param array $config Config options
+ */
+ public function __construct(ArrayAccess|array $data, array $config = [])
+ {
+ $this->setConfig($config);
+ $this->data = $data;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getIdentifier(): array|string|int|null
+ {
+ return $this->get('id');
+ }
+
+ /**
+ * Get data from the identity using object access.
+ *
+ * @param string $field Field in the user data.
+ * @return mixed
+ */
+ public function __get(string $field): mixed
+ {
+ return $this->get($field);
+ }
+
+ /**
+ * Check if the field isset() using object access.
+ *
+ * @param string $field Field in the user data.
+ * @return bool
+ */
+ public function __isset(string $field): bool
+ {
+ return $this->get($field) !== null;
+ }
+
+ /**
+ * Get data from the identity
+ *
+ * @param string $field Field in the user data.
+ * @return mixed
+ */
+ public function get(string $field): mixed
+ {
+ $map = $this->_config['fieldMap'];
+ if (isset($map[$field])) {
+ $field = $map[$field];
+ }
+
+ if (isset($this->data[$field])) {
+ return $this->data[$field];
+ }
+
+ return null;
+ }
+
+ /**
+ * Whether a offset exists
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetexists.php
+ * @param mixed $offset Offset
+ * @return bool
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ return $this->get($offset) !== null;
+ }
+
+ /**
+ * Offset to retrieve
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetget.php
+ * @param mixed $offset Offset
+ * @return \Authentication\IdentityInterface|null
+ */
+ public function offsetGet(mixed $offset): mixed
+ {
+ return $this->get($offset);
+ }
+
+ /**
+ * Offset to set
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetset.php
+ * @param mixed $offset The offset to assign the value to.
+ * @param mixed $value Value
+ * @throws \BadMethodCallException
+ * @return void
+ */
+ public function offsetSet(mixed $offset, mixed $value): void
+ {
+ throw new BadMethodCallException('Identity does not allow wrapped data to be mutated.');
+ }
+
+ /**
+ * Offset to unset
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetunset.php
+ * @param mixed $offset Offset
+ * @throws \BadMethodCallException
+ * @return void
+ */
+ public function offsetUnset(mixed $offset): void
+ {
+ throw new BadMethodCallException('Identity does not allow wrapped data to be mutated.');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getOriginalData(): ArrayAccess|array
+ {
+ return $this->data;
+ }
+
+ /**
+ * Debug info
+ *
+ * @return array
+ */
+ public function __debugInfo(): array
+ {
+ return [
+ 'config' => $this->_config,
+ 'data' => $this->data,
+ ];
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/IdentityInterface.php b/app/vendor/cakephp/authentication/src/IdentityInterface.php
new file mode 100644
index 000000000..48af6e1d1
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/IdentityInterface.php
@@ -0,0 +1,41 @@
+
+ */
+interface IdentityInterface extends ArrayAccess
+{
+ /**
+ * Get the primary key/id field for the identity.
+ *
+ * @return array|string|int|null
+ */
+ public function getIdentifier(): array|string|int|null;
+
+ /**
+ * Gets the original data object.
+ *
+ * @return \ArrayAccess|array
+ */
+ public function getOriginalData(): ArrayAccess|array;
+}
diff --git a/app/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php b/app/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php
new file mode 100644
index 000000000..945af355e
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Middleware/AuthenticationMiddleware.php
@@ -0,0 +1,141 @@
+subject = $subject;
+ $this->container = $container;
+ }
+
+ /**
+ * Callable implementation for the middleware stack.
+ *
+ * @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
+ {
+ $service = $this->getAuthenticationService($request);
+
+ if ($this->subject instanceof ContainerApplicationInterface) {
+ $container = $this->subject->getContainer();
+ $container->add(AuthenticationService::class, $service);
+ } elseif ($this->container) {
+ $this->container->add(AuthenticationService::class, $service);
+ }
+
+ try {
+ $result = $service->authenticate($request);
+ } catch (AuthenticationRequiredException $e) {
+ $body = new Stream('php://memory', 'rw');
+ $body->write($e->getBody());
+ $response = new Response();
+ $response = $response->withStatus($e->getCode())
+ ->withBody($body);
+ foreach ($e->getHeaders() as $header => $value) {
+ $response = $response->withHeader($header, $value);
+ }
+
+ return $response;
+ }
+
+ $request = $request->withAttribute($service->getIdentityAttribute(), $service->getIdentity());
+ $request = $request->withAttribute('authentication', $service);
+ $request = $request->withAttribute('authenticationResult', $result);
+
+ try {
+ $response = $handler->handle($request);
+ $authenticator = $service->getAuthenticationProvider();
+
+ if ($authenticator !== null && !$authenticator instanceof StatelessInterface && $result->getData()) {
+ $return = $service->persistIdentity($request, $response, $result->getData());
+ $response = $return['response'];
+ }
+ } catch (UnauthenticatedException $e) {
+ $url = $service->getUnauthenticatedRedirectUrl($request);
+ if ($url) {
+ return new RedirectResponse($url);
+ }
+ throw $e;
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns AuthenticationServiceInterface instance.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request Server request.
+ * @return \Authentication\AuthenticationServiceInterface
+ * @throws \RuntimeException When authentication method has not been defined.
+ */
+ protected function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
+ {
+ $subject = $this->subject;
+
+ if ($subject instanceof AuthenticationServiceProviderInterface) {
+ $subject = $subject->getAuthenticationService($request);
+ }
+
+ return $subject;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/PasswordHasher/AbstractPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/AbstractPasswordHasher.php
new file mode 100644
index 000000000..f9fbed943
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/PasswordHasher/AbstractPasswordHasher.php
@@ -0,0 +1,60 @@
+ $config Array of config.
+ */
+ public function __construct(array $config = [])
+ {
+ $this->setConfig($config);
+ }
+
+ /**
+ * Returns true if the password need to be rehashed, due to the password being
+ * created with anything else than the passwords generated by this class.
+ *
+ * Returns true by default since the only implementation users should rely
+ * on is the one provided by default in php 5.5+ or any compatible library
+ *
+ * @param string $password The password to verify
+ * @return bool
+ */
+ public function needsRehash(string $password): bool
+ {
+ return password_needs_rehash($password, PASSWORD_DEFAULT);
+ }
+}
diff --git a/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/DefaultPasswordHasher.php
similarity index 80%
rename from app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php
rename to app/vendor/cakephp/authentication/src/PasswordHasher/DefaultPasswordHasher.php
index 35a8246ce..0ec08354a 100644
--- a/app/vendor/cakephp/cakephp/src/Auth/DefaultPasswordHasher.php
+++ b/app/vendor/cakephp/authentication/src/PasswordHasher/DefaultPasswordHasher.php
@@ -11,10 +11,9 @@
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
- * @since 3.0.0
- * @license https://opensource.org/licenses/mit-license.php MIT License
+ * @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Auth;
+namespace Authentication\PasswordHasher;
/**
* Default password hashing class.
@@ -31,9 +30,9 @@ class DefaultPasswordHasher extends AbstractPasswordHasher
* - `hashOptions` - Associative array of options. Check the PHP manual for
* supported options for each hash type. Defaults to empty array.
*
- * @var array
+ * @var array
*/
- protected $_defaultConfig = [
+ protected array $_defaultConfig = [
'hashType' => PASSWORD_DEFAULT,
'hashOptions' => [],
];
@@ -42,17 +41,14 @@ class DefaultPasswordHasher extends AbstractPasswordHasher
* Generates password hash.
*
* @param string $password Plain text password to hash.
- * @return string|false Password hash or false on failure
- * @psalm-suppress InvalidNullableReturnType
- * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#hashing-passwords
+ * @return string Password hash or false on failure.
*/
- public function hash(string $password)
+ public function hash(string $password): string
{
- /** @psalm-suppress NullableReturnStatement */
return password_hash(
$password,
$this->_config['hashType'],
- $this->_config['hashOptions']
+ $this->_config['hashOptions'],
);
}
diff --git a/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/FallbackPasswordHasher.php
similarity index 88%
rename from app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php
rename to app/vendor/cakephp/authentication/src/PasswordHasher/FallbackPasswordHasher.php
index 7c4de0518..6aee5378f 100644
--- a/app/vendor/cakephp/cakephp/src/Auth/FallbackPasswordHasher.php
+++ b/app/vendor/cakephp/authentication/src/PasswordHasher/FallbackPasswordHasher.php
@@ -11,10 +11,9 @@
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
- * @since 3.0.0
- * @license https://opensource.org/licenses/mit-license.php MIT License
+ * @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
-namespace Cake\Auth;
+namespace Authentication\PasswordHasher;
/**
* A password hasher that can use multiple different hashes where only
@@ -26,25 +25,25 @@ class FallbackPasswordHasher extends AbstractPasswordHasher
/**
* Default config for this object.
*
- * @var array
+ * @var array
*/
- protected $_defaultConfig = [
+ protected array $_defaultConfig = [
'hashers' => [],
];
/**
* Holds the list of password hasher objects that will be used
*
- * @var array<\Cake\Auth\AbstractPasswordHasher>
+ * @var array
*/
- protected $_hashers = [];
+ protected array $_hashers = [];
/**
* Constructor
*
* @param array $config configuration options for this object. Requires the
* `hashers` key to be present in the array with a list of other hashers to be
- * used.
+ * used
*/
public function __construct(array $config = [])
{
@@ -63,9 +62,9 @@ public function __construct(array $config = [])
* Uses the first password hasher in the list to generate the hash
*
* @param string $password Plain text password to hash.
- * @return string|false Password hash or false
+ * @return string Password hash
*/
- public function hash(string $password)
+ public function hash(string $password): string
{
return $this->_hashers[0]->hash($password);
}
diff --git a/app/vendor/cakephp/authentication/src/PasswordHasher/LegacyPasswordHasher.php b/app/vendor/cakephp/authentication/src/PasswordHasher/LegacyPasswordHasher.php
new file mode 100644
index 000000000..04a86fe0c
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/PasswordHasher/LegacyPasswordHasher.php
@@ -0,0 +1,78 @@
+ null,
+ 'salt' => true,
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function __construct(array $config = [])
+ {
+ parent::__construct($config);
+ if (Configure::read('debug')) {
+ Debugger::checkSecurityKeys();
+ }
+ if (!class_exists(Security::class)) {
+ throw new RuntimeException('You must install the cakephp/utility dependency to use this password hasher');
+ }
+ }
+
+ /**
+ * Generates password hash.
+ *
+ * @param string $password Plain text password to hash.
+ * @return string Password hash
+ */
+ public function hash(string $password): string
+ {
+ return Security::hash($password, $this->_config['hashType'], $this->_config['salt']);
+ }
+
+ /**
+ * Check hash. Generate hash for user provided password and check against existing hash.
+ *
+ * @param string $password Plain text password to hash.
+ * @param string $hashedPassword Existing hashed password.
+ * @return bool True if hashes match else false.
+ */
+ public function check(string $password, string $hashedPassword): bool
+ {
+ return $hashedPassword === $this->hash($password);
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/PasswordHasher/PasswordHasherFactory.php b/app/vendor/cakephp/authentication/src/PasswordHasher/PasswordHasherFactory.php
new file mode 100644
index 000000000..11bf7ac74
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/PasswordHasher/PasswordHasherFactory.php
@@ -0,0 +1,58 @@
+_passwordHasher === null) {
+ $this->_passwordHasher = new DefaultPasswordHasher();
+ }
+
+ return $this->_passwordHasher;
+ }
+
+ /**
+ * Sets password hasher object.
+ *
+ * @param \Authentication\PasswordHasher\PasswordHasherInterface $passwordHasher Password hasher instance.
+ * @return $this
+ */
+ public function setPasswordHasher(PasswordHasherInterface $passwordHasher)
+ {
+ $this->_passwordHasher = $passwordHasher;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether or not the password stored in the repository for the logged in user
+ * requires to be rehashed with another algorithm
+ *
+ * @return bool
+ */
+ public function needsPasswordRehash(): bool
+ {
+ return $this->_needsPasswordRehash;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/Plugin.php b/app/vendor/cakephp/authentication/src/Plugin.php
new file mode 100644
index 000000000..40c4da55a
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/Plugin.php
@@ -0,0 +1,23 @@
+ false,
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool
+ {
+ $options = $this->_mergeDefaultOptions($options);
+ $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']);
+
+ if (!is_array($loginUrls) || empty($loginUrls)) {
+ throw new InvalidArgumentException('The $loginUrls parameter is empty or not of type array.');
+ }
+
+ // If it's a single route array add to another
+ if (!is_numeric(key($loginUrls))) {
+ $loginUrls = [$loginUrls];
+ }
+
+ foreach ($loginUrls as $validUrl) {
+ $validUrl = Router::url($validUrl, $options['checkFullUrl']);
+
+ if ($validUrl === $url) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/UrlChecker/DefaultUrlChecker.php b/app/vendor/cakephp/authentication/src/UrlChecker/DefaultUrlChecker.php
new file mode 100644
index 000000000..f148a02f4
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/UrlChecker/DefaultUrlChecker.php
@@ -0,0 +1,118 @@
+ false,
+ 'checkFullUrl' => false,
+ ];
+
+ /**
+ * @inheritDoc
+ */
+ public function check(ServerRequestInterface $request, $loginUrls, array $options = []): bool
+ {
+ $options = $this->_mergeDefaultOptions($options);
+
+ $urls = (array)$loginUrls;
+ if (!$urls) {
+ return true;
+ }
+
+ $checker = $this->_getChecker($options);
+
+ $url = $this->_getUrlFromRequest($request, $options['checkFullUrl']);
+
+ foreach ($urls as $validUrl) {
+ if ($checker($validUrl, $url)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Merges given options with the defaults.
+ *
+ * The reason this method exists is that it makes it easy to override the
+ * method and inject additional options without the need to use the
+ * MergeVarsTrait.
+ *
+ * @param array $options Options to merge in
+ * @return array
+ */
+ protected function _mergeDefaultOptions(array $options): array
+ {
+ return $options + $this->_defaultOptions;
+ }
+
+ /**
+ * Gets the checker function name or a callback
+ *
+ * @param array $options Array of options
+ * @return callable
+ */
+ protected function _getChecker(array $options): callable
+ {
+ if (!empty($options['useRegex'])) {
+ return 'preg_match';
+ }
+
+ return function ($validUrl, $url) {
+ return $validUrl === $url;
+ };
+ }
+
+ /**
+ * Returns current url.
+ *
+ * @param \Psr\Http\Message\ServerRequestInterface $request Server Request
+ * @param bool $getFullUrl Get the full URL or just the path
+ * @return string
+ */
+ protected function _getUrlFromRequest(ServerRequestInterface $request, bool $getFullUrl = false): string
+ {
+ $uri = $request->getUri();
+
+ $requestBase = $request->getAttribute('base');
+ if ($requestBase) {
+ $uri = $uri->withPath($requestBase . $uri->getPath());
+ }
+
+ if ($getFullUrl) {
+ return (string)$uri;
+ }
+
+ return $uri->getPath();
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerInterface.php b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerInterface.php
new file mode 100644
index 000000000..797d06098
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerInterface.php
@@ -0,0 +1,35 @@
+ $options Array of options
+ * @return bool
+ */
+ public function check(ServerRequestInterface $request, array|string $loginUrls, array $options = []): bool;
+}
diff --git a/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerTrait.php b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerTrait.php
new file mode 100644
index 000000000..36fe7be83
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/UrlChecker/UrlCheckerTrait.php
@@ -0,0 +1,80 @@
+_getUrlChecker()->check(
+ $request,
+ $this->getConfig('loginUrl'),
+ (array)$this->getConfig('urlChecker'),
+ );
+ }
+
+ /**
+ * Gets the login URL checker
+ *
+ * @return \Authentication\UrlChecker\UrlCheckerInterface
+ */
+ protected function _getUrlChecker(): UrlCheckerInterface
+ {
+ $options = $this->getConfig('urlChecker');
+ if (!is_array($options)) {
+ $options = [
+ 'className' => $options,
+ ];
+ }
+ if (!isset($options['className'])) {
+ $options['className'] = DefaultUrlChecker::class;
+ }
+
+ $className = App::className($options['className'], 'UrlChecker', 'UrlChecker');
+ if ($className === null) {
+ throw new RuntimeException(sprintf('URL checker class `%s` was not found.', $options['className']));
+ }
+
+ $interfaces = class_implements($className);
+
+ if (!isset($interfaces[UrlCheckerInterface::class])) {
+ throw new RuntimeException(sprintf(
+ 'The provided URL checker class `%s` does not implement the `%s` interface.',
+ $options['className'],
+ UrlCheckerInterface::class,
+ ));
+ }
+
+ /** @var \Authentication\UrlChecker\UrlCheckerInterface $obj */
+ $obj = new $className();
+
+ return $obj;
+ }
+}
diff --git a/app/vendor/cakephp/authentication/src/View/Helper/IdentityHelper.php b/app/vendor/cakephp/authentication/src/View/Helper/IdentityHelper.php
new file mode 100644
index 000000000..6c2dc947d
--- /dev/null
+++ b/app/vendor/cakephp/authentication/src/View/Helper/IdentityHelper.php
@@ -0,0 +1,123 @@
+
+ */
+ protected array $_defaultConfig = [
+ 'identityAttribute' => 'identity',
+ ];
+
+ /**
+ * Identity Object
+ *
+ * @var \Authentication\IdentityInterface|null
+ */
+ protected ?IdentityInterface $_identity = null;
+
+ /**
+ * 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
+ {
+ $this->_identity = $this->_View->getRequest()->getAttribute($this->getConfig('identityAttribute'));
+ }
+
+ /**
+ * Gets the id of the current logged in identity
+ *
+ * @return array|string|int|null
+ */
+ public function getId(): array|string|int|null
+ {
+ if ($this->_identity === null) {
+ return null;
+ }
+
+ return $this->_identity->getIdentifier();
+ }
+
+ /**
+ * Checks if a user is logged in
+ *
+ * @return bool
+ */
+ public function isLoggedIn(): bool
+ {
+ return $this->_identity !== null;
+ }
+
+ /**
+ * This check can be used to tell if a record that belongs to some user is
+ * the current logged in user and compare other fields as well
+ *
+ * If you have more complex requirements on visibility checks based on some
+ * kind of permission you should use the Authorization plugin instead:
+ *
+ * https://github.com/cakephp/authorization
+ *
+ * This method is mostly a convenience method for simple cases and not
+ * intended to replace any kind of proper authorization implementation.
+ *
+ * @param string|int $id Identity id to check against
+ * @param string $field Name of the field in the identity data to check against, id by default
+ * @return bool
+ */
+ public function is(int|string $id, string $field = 'id'): bool
+ {
+ return $id === $this->get($field);
+ }
+
+ /**
+ * Gets user data
+ *
+ * @param string|null $key Key of something you want to get from the identity data
+ * @return mixed
+ */
+ public function get(?string $key = null): mixed
+ {
+ if ($this->_identity === null) {
+ return null;
+ }
+
+ if ($key === null) {
+ return $this->_identity->getOriginalData();
+ }
+
+ return Hash::get($this->_identity, $key);
+ }
+}
diff --git a/app/vendor/cakephp/bake/.phive/phars.xml b/app/vendor/cakephp/bake/.phive/phars.xml
new file mode 100644
index 000000000..40973f754
--- /dev/null
+++ b/app/vendor/cakephp/bake/.phive/phars.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/app/vendor/cakephp/bake/Dockerfile b/app/vendor/cakephp/bake/Dockerfile
index 296613920..d1f129524 100644
--- a/app/vendor/cakephp/bake/Dockerfile
+++ b/app/vendor/cakephp/bake/Dockerfile
@@ -1,24 +1,36 @@
-# Basic docker based environment
-# Necessary to trick dokku into building the documentation
-# using dockerfile instead of herokuish
-FROM ubuntu:17.04
-
-# Add basic tools
-RUN apt-get update && \
- apt-get install -y build-essential \
- software-properties-common \
- curl \
- git \
- libxml2 \
- libffi-dev \
- libssl-dev
-
-RUN LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \
- apt-get update && \
- apt-get install -y php7.2-cli php7.2-mbstring php7.2-xml php7.2-zip php7.2-intl php7.2-opcache php7.2-sqlite
-
-WORKDIR /code
-
-VOLUME ["/code"]
-
-CMD [ '/bin/bash' ]
+# ----------------------
+# 1. Build stage
+# ----------------------
+FROM node:24-alpine AS builder
+
+# Git is required because docs/package.json pulls a dependency from GitHub.
+RUN apk add --no-cache git openssh-client
+
+WORKDIR /app/docs
+
+# Copy dependency manifests first to preserve Docker layer caching.
+COPY docs/ ./
+RUN npm ci
+
+# Increase max-old-space-size to avoid memory issues during build
+#ENV NODE_OPTIONS="--max-old-space-size=8192"
+
+# Build the site.
+RUN npm run docs:build
+
+# ----------------------
+# 2. Runtime stage (angie)
+# ----------------------
+FROM docker.angie.software/angie:latest AS runner
+
+# Copy built files
+COPY --from=builder /app/docs/.vitepress/dist /usr/share/angie/html
+
+# Expose port
+EXPOSE 80
+
+# Health check (optional)
+HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1
+
+# Start angie
+CMD ["angie", "-g", "daemon off;"]
diff --git a/app/vendor/cakephp/bake/README.md b/app/vendor/cakephp/bake/README.md
index 3cc152bc7..a403eae02 100644
--- a/app/vendor/cakephp/bake/README.md
+++ b/app/vendor/cakephp/bake/README.md
@@ -20,7 +20,7 @@ composer require --dev cakephp/bake
## Documentation
-You can find the documentation for bake [on its own cookbook](https://book.cakephp.org/bake/2).
+You can find the documentation for bake [on its own cookbook](https://book.cakephp.org/bake/3).
## Testing
diff --git a/app/vendor/cakephp/bake/composer.json b/app/vendor/cakephp/bake/composer.json
index 679346281..6ad33444a 100644
--- a/app/vendor/cakephp/bake/composer.json
+++ b/app/vendor/cakephp/bake/composer.json
@@ -1,34 +1,37 @@
{
"name": "cakephp/bake",
"description": "Bake plugin for CakePHP",
- "type": "cakephp-plugin",
- "keywords": ["cakephp", "bake"],
- "homepage": "https://github.com/cakephp/bake",
"license": "MIT",
+ "type": "cakephp-plugin",
+ "keywords": [
+ "cakephp",
+ "bake",
+ "dev",
+ "cli"
+ ],
"authors": [
{
"name": "CakePHP Community",
"homepage": "https://github.com/cakephp/bake/graphs/contributors"
}
],
+ "homepage": "https://github.com/cakephp/bake",
"support": {
"issues": "https://github.com/cakephp/bake/issues",
"forum": "https://stackoverflow.com/tags/cakephp",
- "irc": "irc://irc.freenode.org/cakephp",
"source": "https://github.com/cakephp/bake"
},
"require": {
- "php": ">=7.2",
- "cakephp/cakephp": "^4.3.0",
- "cakephp/twig-view": "^1.0.2",
- "brick/varexporter": "^0.3.5",
- "nikic/php-parser": "^4.13.2"
+ "php": ">=8.1",
+ "brick/varexporter": "^0.6.0 || ^0.7.0",
+ "cakephp/cakephp": "^5.1",
+ "cakephp/twig-view": "^2.0.2",
+ "nikic/php-parser": "^5.0.0"
},
"require-dev": {
- "cakephp/cakephp-codesniffer": "^4.0",
- "phpunit/phpunit": "^8.5 || ^9.3",
- "cakephp/debug_kit": "^4.1",
- "cakephp/plugin-installer": "^1.3"
+ "cakephp/cakephp-codesniffer": "^5.0.0",
+ "cakephp/debug_kit": "^5.0.0",
+ "phpunit/phpunit": "^10.5.40 || ^11.5.20 || ^12.2.4"
},
"autoload": {
"psr-4": {
@@ -37,12 +40,24 @@
},
"autoload-dev": {
"psr-4": {
+ "Authentication\\": "tests/test_app/Plugin/Authentication/src/",
+ "Authorization\\": "tests/test_app/Plugin/Authorization/src/",
"BakeTest\\": "tests/test_app/Plugin/BakeTest/src/",
- "Company\\Pastry\\": "tests/test_app/Plugin/Company/Pastry/src/",
- "Pastry\\PastryTest\\": "tests/test_app/Plugin/PastryTest/src/",
- "WithBakeSubFolder\\": "tests/test_app/Plugin/WithBakeSubFolder/src/",
"Bake\\Test\\": "tests/",
- "Bake\\Test\\App\\": "tests/test_app/App/"
+ "Bake\\Test\\App\\": "tests/test_app/App/",
+ "Company\\Pastry\\": "tests/test_app/Plugin/Company/Pastry/src/",
+ "FixtureTest\\": "tests/test_app/App/Plugin/FixtureTest/src/",
+ "TestBake\\": "tests/test_app/Plugin/TestBake/src/",
+ "TestBakeTheme\\": "tests/test_app/Plugin/TestBakeTheme/src/",
+ "TestTemplate\\": "tests/test_app/App/Plugin/TestTemplate/src/",
+ "TestTest\\": "tests/test_app/App/Plugin/TestTest/src/",
+ "WithBakeSubFolder\\": "tests/test_app/Plugin/WithBakeSubFolder/src/"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "cakephp/plugin-installer": true,
+ "dealerdirect/phpcodesniffer-composer-installer": true
}
},
"scripts": {
@@ -50,17 +65,16 @@
"@test",
"@cs-check"
],
- "cs-check": "phpcs --parallel=16",
- "cs-fix": "phpcbf --parallel=16",
- "stan": "phpstan analyse && psalm.phar",
- "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:^1.7 psalm/phar:~4.27.0 && mv composer.backup composer.json",
+ "cs-check": "phpcs",
+ "cs-fix": "phpcbf",
+ "phpstan": "tools/phpstan analyse",
+ "stan": "@phpstan",
+ "stan-baseline": "tools/phpstan --generate-baseline",
+ "stan-setup": "phive install",
+ "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" && mv composer.backup composer.json",
+ "rector-check": "vendor/bin/rector process --dry-run",
+ "rector-fix": "vendor/bin/rector process",
"test": "phpunit",
"test-coverage": "phpunit --coverage-clover=clover.xml"
- },
- "config": {
- "allow-plugins": {
- "cakephp/plugin-installer": true,
- "dealerdirect/phpcodesniffer-composer-installer": true
- }
}
}
diff --git a/app/vendor/cakephp/bake/docs.Dockerfile b/app/vendor/cakephp/bake/docs.Dockerfile
deleted file mode 100644
index 4f3ca473c..000000000
--- a/app/vendor/cakephp/bake/docs.Dockerfile
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generate the HTML output.
-FROM ghcr.io/cakephp/docs-builder as builder
-
-# Copy entire repo in with .git so we can build all versions in one image.
-COPY docs /data/docs
-
-RUN cd /data/docs-builder \
- && make website LANGS="en es fr ja pt ru" SOURCE=/data/docs DEST=/data/website/
-
-# Build a small nginx container with just the static site in it.
-FROM ghcr.io/cakephp/docs-builder:runtime as runtime
-
-# Configure search index script
-ENV LANGS="en es fr ja pt ru"
-ENV SEARCH_SOURCE="/usr/share/nginx/html"
-ENV SEARCH_URL_PREFIX="/bake/2"
-
-COPY --from=builder /data/docs /data/docs
-COPY --from=builder /data/website /data/website
-COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf
-
-# Copy docs into place.
-RUN cp -R /data/website/html/* /usr/share/nginx/html \
- && rm -rf /data/website
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/config.js b/app/vendor/cakephp/bake/docs/.vitepress/config.js
new file mode 100644
index 000000000..5e8ac6185
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/config.js
@@ -0,0 +1,94 @@
+import baseConfig from '@cakephp/docs-skeleton/config'
+import { createRequire } from 'module'
+
+const require = createRequire(import.meta.url)
+const tocEn = require('./toc_en.json')
+const tocEs = require('./toc_es.json')
+const tocFr = require('./toc_fr.json')
+const tocJa = require('./toc_ja.json')
+const tocPt = require('./toc_pt.json')
+const tocRu = require('./toc_ru.json')
+
+const versions = {
+ text: '3.x',
+ items: [
+ { text: '3.x (current)', link: 'https://book.cakephp.org/bake/3/', target: '_self' },
+ { text: '2.x', link: 'https://book.cakephp.org/bake/2.x/', target: '_self' },
+ { text: '1.x', link: 'https://book.cakephp.org/bake/1.x/', target: '_self' },
+ ],
+}
+
+export default {
+ extends: baseConfig,
+ srcDir: '.',
+ title: 'Bake',
+ description: 'CakePHP Bake Documentation',
+ base: '/bake/3/',
+ rewrites: {
+ 'en/:slug*': ':slug*',
+ },
+ sitemap: {
+ hostname: 'https://book.cakephp.org/bake/3/',
+ },
+ themeConfig: {
+ siteTitle: false,
+ pluginName: "Bake",
+ socialLinks: [
+ { icon: 'github', link: 'https://github.com/cakephp/bake' },
+ ],
+ editLink: {
+ pattern: 'https://github.com/cakephp/bake/edit/3.x/docs/:path',
+ text: 'Edit this page on GitHub',
+ },
+ sidebar: tocEn,
+ nav: [
+ { text: 'CakePHP', link: 'https://cakephp.org' },
+ { text: 'API', link: 'https://api.cakephp.org/bake/' },
+ { ...versions },
+ ],
+ },
+ locales: {
+ root: {
+ label: 'English',
+ lang: 'en',
+ themeConfig: {
+ sidebar: tocEn,
+ },
+ },
+ es: {
+ label: 'Español',
+ lang: 'es',
+ themeConfig: {
+ sidebar: tocEs,
+ },
+ },
+ fr: {
+ label: 'Français',
+ lang: 'fr',
+ themeConfig: {
+ sidebar: tocFr,
+ },
+ },
+ ja: {
+ label: '日本語',
+ lang: 'ja',
+ themeConfig: {
+ sidebar: tocJa,
+ },
+ },
+ pt: {
+ label: 'Português',
+ lang: 'pt',
+ themeConfig: {
+ sidebar: tocPt,
+ },
+ },
+ ru: {
+ label: 'Русский',
+ lang: 'ru',
+ themeConfig: {
+ sidebar: tocRu,
+ },
+ },
+ },
+}
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/theme/index.js b/app/vendor/cakephp/bake/docs/.vitepress/theme/index.js
new file mode 100644
index 000000000..e33e19ec9
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/theme/index.js
@@ -0,0 +1 @@
+export { default } from '@cakephp/docs-skeleton'
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/toc_en.json b/app/vendor/cakephp/bake/docs/.vitepress/toc_en.json
new file mode 100644
index 000000000..ae8a22c60
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/toc_en.json
@@ -0,0 +1,13 @@
+{
+ "/": [
+ {
+ "text": "CakePHP Bake",
+ "collapsed": false,
+ "items": [
+ { "text": "Bake Console", "link": "/" },
+ { "text": "Code Generation with Bake", "link": "/usage" },
+ { "text": "Extending Bake", "link": "/development" }
+ ]
+ }
+ ]
+}
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/toc_es.json b/app/vendor/cakephp/bake/docs/.vitepress/toc_es.json
new file mode 100644
index 000000000..98b6e2cb0
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/toc_es.json
@@ -0,0 +1,13 @@
+{
+ "/es/": [
+ {
+ "text": "CakePHP Bake",
+ "collapsed": false,
+ "items": [
+ { "text": "Consola Bake", "link": "/es/" },
+ { "text": "Crear código con Bake", "link": "/es/usage" },
+ { "text": "Extending Bake", "link": "/es/development" }
+ ]
+ }
+ ]
+}
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/toc_fr.json b/app/vendor/cakephp/bake/docs/.vitepress/toc_fr.json
new file mode 100644
index 000000000..eceb87131
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/toc_fr.json
@@ -0,0 +1,13 @@
+{
+ "/fr/": [
+ {
+ "text": "CakePHP Bake",
+ "collapsed": false,
+ "items": [
+ { "text": "Console Bake", "link": "/fr/" },
+ { "text": "Génération de code avec Bake", "link": "/fr/usage" },
+ { "text": "Étendre Bake", "link": "/fr/development" }
+ ]
+ }
+ ]
+}
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/toc_ja.json b/app/vendor/cakephp/bake/docs/.vitepress/toc_ja.json
new file mode 100644
index 000000000..a459e238c
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/toc_ja.json
@@ -0,0 +1,13 @@
+{
+ "/ja/": [
+ {
+ "text": "CakePHP Bake",
+ "collapsed": false,
+ "items": [
+ { "text": "Bake コンソール", "link": "/ja/" },
+ { "text": "Bake でコード生成", "link": "/ja/usage" },
+ { "text": "Bake の拡張", "link": "/ja/development" }
+ ]
+ }
+ ]
+}
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/toc_pt.json b/app/vendor/cakephp/bake/docs/.vitepress/toc_pt.json
new file mode 100644
index 000000000..70af9e386
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/toc_pt.json
@@ -0,0 +1,13 @@
+{
+ "/pt/": [
+ {
+ "text": "CakePHP Bake",
+ "collapsed": false,
+ "items": [
+ { "text": "Console Bake", "link": "/pt/" },
+ { "text": "Geração de Código com Bake", "link": "/pt/usage" },
+ { "text": "Estendendo o Bake", "link": "/pt/development" }
+ ]
+ }
+ ]
+}
diff --git a/app/vendor/cakephp/bake/docs/.vitepress/toc_ru.json b/app/vendor/cakephp/bake/docs/.vitepress/toc_ru.json
new file mode 100644
index 000000000..38d8ea36d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/.vitepress/toc_ru.json
@@ -0,0 +1,13 @@
+{
+ "/ru/": [
+ {
+ "text": "CakePHP Bake",
+ "collapsed": false,
+ "items": [
+ { "text": "Консоль Bake", "link": "/ru/" },
+ { "text": "Генерация кода с помощью Bake", "link": "/ru/usage" },
+ { "text": "Расширение возможностей Bake", "link": "/ru/development" }
+ ]
+ }
+ ]
+}
diff --git a/app/vendor/cakephp/bake/docs/config/all.py b/app/vendor/cakephp/bake/docs/config/all.py
deleted file mode 100644
index 260522630..000000000
--- a/app/vendor/cakephp/bake/docs/config/all.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# Global configuration information used across all the
-# translations of documentation.
-#
-# Import the base theme configuration
-from cakephpsphinx.config.all import *
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-
-# The full version, including alpha/beta/rc tags.
-release = '2.x'
-
-# The search index version.
-search_version = 'bake-2'
-
-# The marketing display name for the book.
-version_name = ''
-
-# Project name shown in the black header bar
-project = 'CakePHP Bake'
-
-# Other versions that display in the version picker menu.
-version_list = [
- {'name': '1.x', 'number': '/bake/1.x', 'title': '1.x'},
- {'name': '2.x', 'number': '/bake/2.x', 'title': '2.x', 'current': True},
-]
-
-# Languages available.
-languages = ['en', 'es', 'fr', 'ja', 'pt', 'ru']
-
-# The GitHub branch name for this version of the docs
-# for edit links to point at.
-branch = '2.x'
-
-# Current version being built
-version = '2.x'
-
-# Language in use for this directory.
-language = 'en'
-
-show_root_link = True
-
-repository = 'cakephp/bake'
-
-source_path = 'docs/'
-
-hide_page_contents = ('search', '404', 'contents')
diff --git a/app/vendor/cakephp/bake/docs/en/conf.py b/app/vendor/cakephp/bake/docs/en/conf.py
deleted file mode 100644
index f638bda22..000000000
--- a/app/vendor/cakephp/bake/docs/en/conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import sys, os
-
-# Append the top level directory of the docs, so we can import from the config dir.
-sys.path.insert(0, os.path.abspath('..'))
-
-# Pull in all the configuration options defined in the global config file..
-from config.all import *
-
-language = 'en'
diff --git a/app/vendor/cakephp/bake/docs/en/contents.rst b/app/vendor/cakephp/bake/docs/en/contents.rst
deleted file mode 100644
index 08c3e957c..000000000
--- a/app/vendor/cakephp/bake/docs/en/contents.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. toctree::
- :maxdepth: 2
- :caption: CakePHP Bake
-
- /index
- /usage
- /development
diff --git a/app/vendor/cakephp/bake/docs/en/development.md b/app/vendor/cakephp/bake/docs/en/development.md
new file mode 100644
index 000000000..bc7d608a8
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/en/development.md
@@ -0,0 +1,315 @@
+# Extending Bake
+
+Bake features an extensible architecture that allows your application or plugins
+to modify or add to the base functionality.
+Bake makes use of a dedicated view class that uses the [Twig](https://twig.symfony.com/) template engine.
+
+## Bake Events
+
+As a view class, `BakeView` emits the same events as any other view class, plus one extra initialize event.
+However, whereas standard view classes use the event prefix `View.`, `BakeView` uses the event prefix `Bake.`.
+
+The initialize event can be used to make changes that apply to all baked output.
+For example, to add another helper to the bake view class:
+
+```php
+on('Bake.initialize', function (EventInterface $event) {
+ $view = $event->getSubject();
+
+ // In my bake templates, allow the use of the MySpecial helper
+ $view->loadHelper('MySpecial', ['some' => 'config']);
+
+ // And add an $author variable so it's always available
+ $view->set('author', 'Andy');
+});
+```
+
+Bake events can also be handy for making small changes to existing templates.
+For example, to change the variable names used when baking controller and template files, listen for `Bake.beforeRender`:
+
+```php
+on('Bake.beforeRender', function (EventInterface $event) {
+ $view = $event->getSubject();
+
+ // Use $rows for the main data variable in indexes
+ if ($view->get('pluralName')) {
+ $view->set('pluralName', 'rows');
+ }
+ if ($view->get('pluralVar')) {
+ $view->set('pluralVar', 'rows');
+ }
+
+ // Use $theOne for the main data variable in view/edit
+ if ($view->get('singularName')) {
+ $view->set('singularName', 'theOne');
+ }
+ if ($view->get('singularVar')) {
+ $view->set('singularVar', 'theOne');
+ }
+});
+```
+
+You may also scope `Bake.beforeRender` and `Bake.afterRender` events to a specific generated file.
+For instance, if you want to add specific actions to your `UsersController` when generating from a `Controller/controller.twig` file:
+
+```php
+on(
+ 'Bake.beforeRender.Controller.controller',
+ function (EventInterface $event) {
+ $view = $event->getSubject();
+ if ($view->get('name') === 'Users') {
+ // add the login and logout actions to the Users controller
+ $view->set('actions', [
+ 'login',
+ 'logout',
+ 'index',
+ 'view',
+ 'add',
+ 'edit',
+ 'delete',
+ ]);
+ }
+ }
+);
+```
+
+By scoping event listeners to specific bake templates, you can simplify your bake-related event logic and provide callbacks that are easier to test.
+
+## Bake Template Syntax
+
+Bake template files use the [Twig](https://twig.symfony.com/) template syntax.
+
+For example, when baking a command like this:
+
+```bash
+bin/cake bake command Foo
+```
+
+The template used at `vendor/cakephp/bake/templates/bake/Command/command.twig` looks like this:
+
+```php
+Test->classSuffixes[$this->name()])) {
+ $this->Test->classSuffixes[$this->name()] = 'Foo';
+ }
+
+ $name = ucfirst($this->name());
+ if (!isset($this->Test->classTypes[$name])) {
+ $this->Test->classTypes[$name] = 'Foo';
+ }
+
+ parent::bakeTest($className, $args, $io);
+}
+```
+
+- The **class suffix** is appended to the name provided in your `bake` call. In the example above, that would create `ExampleFooTest.php`.
+- The **class type** is the sub-namespace used to reach your file relative to the app or plugin you are baking into. In the example above, that would create the test namespace `App\Test\TestCase\Foo`.
+
+## Configuring the BakeView Class
+
+Bake commands use the `BakeView` class to render templates.
+You can access the instance by listening to the `Bake.initialize` event:
+
+```php
+on(
+ 'Bake.initialize',
+ function ($event, $view) {
+ $view->loadHelper('Foo');
+ }
+);
+```
diff --git a/app/vendor/cakephp/bake/docs/en/development.rst b/app/vendor/cakephp/bake/docs/en/development.rst
deleted file mode 100644
index 735c4183f..000000000
--- a/app/vendor/cakephp/bake/docs/en/development.rst
+++ /dev/null
@@ -1,343 +0,0 @@
-Extending Bake
-##############
-
-Bake features an extensible architecture that allows your application or plugins
-to modify or add-to the base functionality. Bake makes use of a dedicated
-view class which uses the `Twig `_ template engine.
-
-Bake Events
-===========
-
-As a view class, ``BakeView`` emits the same events as any other view class,
-plus one extra initialize event. However, whereas standard view classes use the
-event prefix "View.", ``BakeView`` uses the event prefix "Bake.".
-
-The initialize event can be used to make changes which apply to all baked
-output, for example to add another helper to the bake view class this event can
-be used::
-
- on('Bake.initialize', function (EventInterface $event) {
- $view = $event->getSubject();
-
- // In my bake templates, allow the use of the MySpecial helper
- $view->loadHelper('MySpecial', ['some' => 'config']);
-
- // And add an $author variable so it's always available
- $view->set('author', 'Andy');
- });
-
-Bake events can be handy for making small changes to existing templates.
-For example, to change the variable names used when baking controller/template
-files one can use a function listening for ``Bake.beforeRender`` to modify the
-variables used in the bake templates::
-
- on('Bake.beforeRender', function (EventInterface $event) {
- $view = $event->getSubject();
-
- // Use $rows for the main data variable in indexes
- if ($view->get('pluralName')) {
- $view->set('pluralName', 'rows');
- }
- if ($view->get('pluralVar')) {
- $view->set('pluralVar', 'rows');
- }
-
- // Use $theOne for the main data variable in view/edit
- if ($view->get('singularName')) {
- $view->set('singularName', 'theOne');
- }
- if ($view->get('singularVar')) {
- $view->set('singularVar', 'theOne');
- }
- });
-
-You may also scope the ``Bake.beforeRender`` and ``Bake.afterRender`` events to
-a specific generated file. For instance, if you want to add specific actions to
-your UsersController when generating from a **Controller/controller.twig** file,
-you can use the following event::
-
- on(
- 'Bake.beforeRender.Controller.controller',
- function (EventInterface $event) {
- $view = $event->getSubject();
- if ($view->get('name') === 'Users') {
- // add the login and logout actions to the Users controller
- $view->set('actions', [
- 'login',
- 'logout',
- 'index',
- 'view',
- 'add',
- 'edit',
- 'delete'
- ]);
- }
- }
- );
-
-By scoping event listeners to specific bake templates, you can simplify your
-bake related event logic and provide callbacks that are easier to test.
-
-Bake Template Syntax
-====================
-
-Bake template files use the `Twig `__ template syntax.
-
-So, for example, when baking a command like so:
-
-.. code-block:: bash
-
- bin/cake bake command Foo
-
-The template used (**vendor/cakephp/bake/templates/bake/Command/command.twig**)
-looks like this::
-
- Test->classSuffixes[$this->name()])) {
- $this->Test->classSuffixes[$this->name()] = 'Foo';
- }
-
- $name = ucfirst($this->name());
- if (!isset($this->Test->classTypes[$name])) {
- $this->Test->classTypes[$name] = 'Foo';
- }
-
- return parent::bakeTest($className);
- }
-
-* The **class suffix** will be appened to the name provided in your ``bake``
- call. In the previous example, it would create a ``ExampleFooTest.php`` file.
-* The **class type** will be the sub-namespace used that will lead to your
- file (relative to the app or the plugin you are baking into). In the previous
- example, it would create your test with the namespace ``App\Test\TestCase\Foo``.
-
-Configuring the BakeView class
-==============================
-
-The bake commands use the ``BakeView`` class to render the templates. You can
-access the instance by listening to the ``Bake.initialize`` event. For example, here's
-how you can load your own helper so that it can be used in bake templates::
-
- on(
- 'Bake.initialize',
- function ($event, $view) {
- $view->loadHelper('Foo');
- }
- );
-
-.. meta::
- :title lang=en: Extending Bake
- :keywords lang=en: command line interface, development, bake view, bake template syntax, twig, erb tags, percent tags
diff --git a/app/vendor/cakephp/bake/docs/en/index.md b/app/vendor/cakephp/bake/docs/en/index.md
new file mode 100644
index 000000000..4f8b7808b
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/en/index.md
@@ -0,0 +1,25 @@
+# Bake Console
+
+CakePHP's bake console is another effort to get you up and running in CakePHP fast.
+The bake console can create any of CakePHP's basic ingredients: models,
+behaviors, views, helpers, controllers, components, test cases, fixtures, and plugins.
+Bake can create far more than skeleton classes and is a natural next step once an application has been scaffolded.
+
+## Installation
+
+Before trying to use or extend Bake, make sure it is installed in your application.
+Bake is provided as a plugin that you can install with Composer:
+
+```bash
+composer require --dev cakephp/bake:"^3.0"
+```
+
+The above installs Bake as a development dependency, so it will not be installed during production deployments.
+
+When using Twig templates, make sure you are loading the `Cake/TwigView` plugin with its bootstrap.
+You can also omit it completely, which makes the Bake plugin load it on demand.
+
+## Documentation Map
+
+- [Code Generation with Bake](/usage) covers running the console, listing commands, baking models and enums, and changing bake themes.
+- [Extending Bake](/development) covers events, Twig templates, custom themes, application template overrides, and creating custom bake commands.
diff --git a/app/vendor/cakephp/bake/docs/en/index.rst b/app/vendor/cakephp/bake/docs/en/index.rst
deleted file mode 100644
index 28899b7b0..000000000
--- a/app/vendor/cakephp/bake/docs/en/index.rst
+++ /dev/null
@@ -1,28 +0,0 @@
-Bake Console
-############
-
-CakePHP's bake console is another effort to get you up and running in CakePHP
-– fast. The bake console can create any of CakePHP's basic ingredients: models,
-behaviors, views, helpers, controllers, components, test cases, fixtures and plugins.
-And we aren't just talking skeleton classes: Bake can create a fully functional
-application in just a few minutes. In fact, Bake is a natural step to take once
-an application has been scaffolded.
-
-Installation
-============
-
-Before trying to use or extend bake, make sure it is installed in your
-application. Bake is provided as a plugin that you can install with Composer::
-
- composer require --dev cakephp/bake:"^2.0"
-
-The above will install bake as a development dependency. This means that it will
-not be installed when you do production deployments.
-
-When using the Twig templates make sure you are loading the
-``Cake/TwigView`` plugin with its bootstrap. You can also omit it
-completely which then makes Bake plugin load this plugin on demand.
-
-.. meta::
- :title lang=en: Bake Console
- :keywords lang=en: command line interface,development,bake view, bake template syntax,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/en/usage.md b/app/vendor/cakephp/bake/docs/en/usage.md
new file mode 100644
index 000000000..76b226d9f
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/en/usage.md
@@ -0,0 +1,124 @@
+# Code Generation with Bake
+
+The Bake console is run using the PHP CLI.
+If you have problems running the script, ensure that:
+
+1. You have the PHP CLI installed and that it has the proper modules enabled, such as MySQL and `intl`.
+2. If the database host is `localhost`, try `127.0.0.1` instead, as `localhost` can cause issues with PHP CLI.
+3. Depending on how your computer is configured, you may need to set execute rights on the Cake shell script to call it using `bin/cake bake`.
+
+Before running Bake you should make sure you have at least one database connection configured.
+
+You can get the list of available bake commands by running `bin/cake bake --help`.
+For Windows usage use `bin\cake bake --help`:
+
+```bash
+$ bin/cake bake --help
+Current Paths:
+
+* app: src/
+* root: /path/to/your/app/
+* core: /path/to/your/app/vendor/cakephp/cakephp/
+
+Available Commands:
+
+Bake:
+- bake all
+- bake behavior
+- bake cell
+- bake command
+- bake command_helper
+- bake component
+- bake controller
+- bake controller all
+- bake enum
+- bake fixture
+- bake fixture all
+- bake form
+- bake helper
+- bake mailer
+- bake middleware
+- bake model
+- bake model all
+- bake plugin
+- bake template
+- bake template all
+- bake test
+
+To run a command, type `cake command_name [args|options]`
+To get help on a specific command, type `cake command_name --help`
+```
+
+## Bake Models
+
+Models are generically baked from existing database tables.
+CakePHP conventions apply, so Bake detects relations based on `thing_id` foreign keys to `things` tables with their `id` primary keys.
+
+For non-conventional relations, you can use references in constraints or foreign key definitions for Bake to detect relations:
+
+```php
+->addForeignKey('billing_country_id', 'countries') // defaults to `id`
+->addForeignKey('shipping_country_id', 'countries', 'cid')
+```
+
+## Bake Enums
+
+You can use Bake to generate [backed enums](https://www.php.net/manual/en/language.enumerations.backed.php) for use in your models.
+Enums are placed in `src/Model/Enum/` and implement `EnumLabelInterface`, which provides a `label()` method for human-readable display.
+
+To bake a string-backed enum:
+
+```bash
+bin/cake bake enum ArticleStatus draft,published,archived
+```
+
+This generates `src/Model/Enum/ArticleStatus.php`:
+
+```php
+namespace App\Model\Enum;
+
+use Cake\Database\Type\EnumLabelInterface;
+use Cake\Utility\Inflector;
+
+enum ArticleStatus: string implements EnumLabelInterface
+{
+ case Draft = 'draft';
+ case Published = 'published';
+ case Archived = 'archived';
+
+ public function label(): string
+ {
+ return Inflector::humanize(Inflector::underscore($this->name));
+ }
+}
+```
+
+For int-backed enums, use the `-i` option and provide values with colons:
+
+```bash
+bin/cake bake enum Priority low:1,medium:2,high:3 -i
+```
+
+This generates an int-backed enum:
+
+```php
+enum Priority: int implements EnumLabelInterface
+{
+ case Low = 1;
+ case Medium = 2;
+ case High = 3;
+
+ // ...
+}
+```
+
+You can also bake enums into plugins:
+
+```bash
+bin/cake bake enum MyPlugin.OrderStatus pending,processing,shipped
+```
+
+## Bake Themes
+
+The `theme` option is common to all bake commands and allows changing the bake template files used when baking.
+To create your own templates, see [Creating a Bake Theme](/development#creating-a-bake-theme).
diff --git a/app/vendor/cakephp/bake/docs/en/usage.rst b/app/vendor/cakephp/bake/docs/en/usage.rst
deleted file mode 100644
index d68a745f0..000000000
--- a/app/vendor/cakephp/bake/docs/en/usage.rst
+++ /dev/null
@@ -1,62 +0,0 @@
-Code Generation with Bake
-#########################
-
-The Bake console is run using the PHP CLI (command line interface).
-If you have problems running the script, ensure that:
-
-#. You have the PHP CLI installed and that it has the proper modules enabled
- (eg: MySQL, intl).
-#. Users also might have issues if the database host is 'localhost' and should
- try '127.0.0.1' instead, as localhost can cause issues with PHP CLI.
-#. Depending on how your computer is configured, you may have to set execute
- rights on the cake bash script to call it using ``bin/cake bake``.
-
-Before running bake you should make sure you have at least one database
-connection configured.
-
-You can get the list of available bake command by running ``bin/cake bake --help``
-(For Windows usage ``bin\cake bake --help``) ::
-
- $ bin/cake bake --help
- Current Paths:
-
- * app: src/
- * root: /path/to/your/app/
- * core: /path/to/your/app/vendor/cakephp/cakephp/
-
- Available Commands:
-
- Bake:
- - bake all
- - bake behavior
- - bake cell
- - bake command
- - bake component
- - bake controller
- - bake controller all
- - bake fixture
- - bake fixture all
- - bake form
- - bake helper
- - bake mailer
- - bake middleware
- - bake model
- - bake model all
- - bake plugin
- - bake template
- - bake template all
- - bake test
-
- To run a command, type `cake command_name [args|options]`
- To get help on a specific command, type `cake command_name --help`
-
-Bake Themes
-===========
-
-The theme option is common to all bake commands, and allows changing the bake
-template files used when baking. To create your own templates, see the
-:ref:`bake theme creation documentation `.
-
-.. meta::
- :title lang=en: Code Generation with Bake
- :keywords lang=en: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql
diff --git a/app/vendor/cakephp/bake/docs/es/conf.py b/app/vendor/cakephp/bake/docs/es/conf.py
deleted file mode 100644
index 4691ece6a..000000000
--- a/app/vendor/cakephp/bake/docs/es/conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import sys, os
-
-# Append the top level directory of the docs, so we can import from the config dir.
-sys.path.insert(0, os.path.abspath('..'))
-
-# Pull in all the configuration options defined in the global config file..
-from config.all import *
-
-language = 'es'
diff --git a/app/vendor/cakephp/bake/docs/es/contents.rst b/app/vendor/cakephp/bake/docs/es/contents.rst
deleted file mode 100644
index 08c3e957c..000000000
--- a/app/vendor/cakephp/bake/docs/es/contents.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. toctree::
- :maxdepth: 2
- :caption: CakePHP Bake
-
- /index
- /usage
- /development
diff --git a/app/vendor/cakephp/bake/docs/es/development.md b/app/vendor/cakephp/bake/docs/es/development.md
new file mode 100644
index 000000000..a6ecced69
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/es/development.md
@@ -0,0 +1,9 @@
+# Extending Bake
+
+::: info
+La documentación no es compatible actualmente con el idioma español en esta página.
+
+Por favor, siéntase libre de enviarnos un pull request en [GitHub](https://github.com/cakephp/bake) o utilizar el botón de edición de la documentación para proponer cambios.
+
+Puede consultar la versión en inglés desde el selector de idioma superior para obtener información sobre este tema.
+:::
diff --git a/app/vendor/cakephp/bake/docs/es/development.rst b/app/vendor/cakephp/bake/docs/es/development.rst
deleted file mode 100644
index 7f717d051..000000000
--- a/app/vendor/cakephp/bake/docs/es/development.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-Extending Bake
-##############
-
-.. note::
- La documentación no es compatible actualmente con el idioma español en esta página.
-
- Por favor, siéntase libre de enviarnos un pull request en
- `Github `_ o utilizar el botón **Improve this Doc** para proponer directamente los cambios.
-
- Usted puede hacer referencia a la versión en Inglés en el menú de selección superior
- para obtener información sobre el tema de esta página.
-
-.. _creating-a-bake-theme:
-
-.. meta::
- :title lang=es: Extending Bake
- :keywords lang=es: command line interface,development,bake view, bake template syntax,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/es/index.md b/app/vendor/cakephp/bake/docs/es/index.md
new file mode 100644
index 000000000..6e7328672
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/es/index.md
@@ -0,0 +1,24 @@
+# Consola Bake
+
+La consola Bake de CakePHP permite preparar y ejecutar CakePHP rápidamente.
+Puede crear muchos de los ingredientes básicos de CakePHP, como modelos,
+behaviours, vistas, helpers, controladores, componentes, casos de prueba,
+fixtures y plugins.
+Bake puede generar mucho más que esqueletos de clases y es un paso natural después de crear la base de la aplicación.
+
+## Instalación
+
+Antes de intentar utilizar o extender Bake, asegúrate de que está instalado en tu aplicación.
+Bake se distribuye como un plugin que puedes instalar con Composer:
+
+```bash
+composer require --dev cakephp/bake:~1.0
+```
+
+La instrucción anterior instalará Bake como una dependencia de desarrollo.
+Esto significa que no será instalado cuando hagas despliegues en producción.
+
+## Mapa de documentación
+
+- [Crear código con Bake](/es/usage) cubre la ejecución del CLI, el listado de tareas y los temas de Bake.
+- [Extending Bake](/es/development) enlaza al contenido disponible para ampliaciones y personalización.
diff --git a/app/vendor/cakephp/bake/docs/es/index.rst b/app/vendor/cakephp/bake/docs/es/index.rst
deleted file mode 100644
index 80f1fa654..000000000
--- a/app/vendor/cakephp/bake/docs/es/index.rst
+++ /dev/null
@@ -1,31 +0,0 @@
-Consola bake
-############
-
-La consola bake de CakePHP es otro esfuerzo para preparar y ejecutar CakePHP rápidamente.
-
-La consola bake puede crear cualquiera de los ingredientes básicos de CakePHP:
-modelos, behaviours, vistas, helpers, controladores, componentes, casos de
-prueba, fixtures y plugins.
-
-Y no hablamos sólo de esqueletos de clases: Bake puede crear una aplicación
-totalmente funcional en solo un par de minutos.
-
-De hecho, Bake es un paso natural a dar una vez ha sido creado el esqueleto de
-la aplicación.
-
-Instalación
-===========
-
-Antes de intentar utilizar o extender bake asegúrate de que está instalado en tu
-aplicación.
-
-Bake está incluido como un plugin que puedes instalar con Composer::
-
- composer require --dev cakephp/bake:~1.0
-
-La instrucción anterior instalará bake como una dependencia de desarrollo. Esto
-significa que no será instalado cuando hagas despliegues en producción.
-
-.. meta::
- :title lang=es: Consola Bake
- :keywords lang=es: interfaz de línea de comando,desarrollo,bake vista, bake sintaxis plantilla,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/es/usage.md b/app/vendor/cakephp/bake/docs/es/usage.md
new file mode 100644
index 000000000..c1862006d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/es/usage.md
@@ -0,0 +1,100 @@
+# Crear código con Bake
+
+La consola de Cake se ejecuta usando PHP CLI.
+Si tiene problemas para ejecutar el script, asegúrese de:
+
+1. Tener instalado PHP CLI y los módulos correspondientes habilitados, por ejemplo MySQL e `intl`.
+2. Si el host de la base de datos es `localhost`, intentar la conexión con `127.0.0.1`, ya que en algunos casos PHP CLI tiene problemas al usar `localhost`.
+3. Dependiendo de cómo esté configurado su equipo, la ejecución de `bin/cake bake` puede requerir permisos de ejecución.
+
+Antes de comenzar, asegúrese de disponer al menos de una conexión a base de datos configurada.
+
+Para comenzar con la ejecución del comando puede abrir la consola y ejecutar `Cake bake`.
+
+1. Ir a Inicio > Ejecutar.
+2. Escribir `cmd` y presionar Enter.
+3. Navegar hasta la carpeta de instalación de Cake.
+4. Acceder a la carpeta `bin`.
+5. Escribir `Cake bake`, lo cual deberá devolver un listado con las tareas disponibles.
+
+El resultado debería ser algo similar a lo siguiente:
+
+```bash
+$ bin/cake bake
+
+Welcome to CakePHP v3.1.6 Console
+---------------------------------------------------------------
+App : src
+Path: /var/www/cakephp.dev/src/
+PHP: 5.5.8
+---------------------------------------------------------------
+The following commands can be used to generate skeleton code for your application.
+
+Available bake commands:
+
+- all
+- behavior
+- cell
+- component
+- controller
+- fixture
+- form
+- helper
+- mailer
+- migration
+- migration_snapshot
+- model
+- plugin
+- template
+- test
+
+By using 'cake bake [name]' you can invoke a specific bake task.
+```
+
+Puede obtener más información sobre lo que realiza cada tarea y sus opciones usando `--help`:
+
+```bash
+$ bin/cake bake controller --help
+
+Welcome to CakePHP v3.1.6 Console
+---------------------------------------------------------------
+App : src
+Path: /var/www/cakephp.dev/src/
+---------------------------------------------------------------
+Bake a controller skeleton.
+
+Usage:
+cake bake controller [subcommand] [options] []
+
+Subcommands:
+
+all Bake all controllers with CRUD methods.
+
+To see help on a subcommand use `cake bake controller [subcommand] --help`
+
+Options:
+
+--help, -h Display this help.
+--verbose, -v Enable verbose output.
+--quiet, -q Enable quiet output.
+--plugin, -p Plugin to bake into.
+--force, -f Force overwriting existing files without prompting.
+--connection, -c The datasource connection to get data from.
+ (default: default)
+--theme, -t The theme to use when baking code.
+--components The comma separated list of components to use.
+--helpers The comma separated list of helpers to use.
+--prefix The namespace/routing prefix to use.
+--no-test Do not generate a test skeleton.
+--no-actions Do not generate basic CRUD action methods.
+
+Arguments:
+
+name Name of the controller to bake. Can use Plugin.name to bake
+ controllers into plugins. (optional)
+```
+
+## Temas Bake / Templates
+
+La opción `theme` es genérica para todos los comandos Bake y permite cambiar los templates utilizados para generar los archivos finales.
+Para crear sus propios templates, vea [Creating a Bake Theme](/es/development#creating-a-bake-theme).
diff --git a/app/vendor/cakephp/bake/docs/es/usage.rst b/app/vendor/cakephp/bake/docs/es/usage.rst
deleted file mode 100644
index dad6ffd95..000000000
--- a/app/vendor/cakephp/bake/docs/es/usage.rst
+++ /dev/null
@@ -1,115 +0,0 @@
-Crear código con Bake
-#####################
-
-La consola de CAKE se ejecuta usando PHP CLI (command line interface).
-Si tiene problemas para ejecutar el script, asegurese de:
-
-#. Tener instalado el PHP CLI y que estén los módulos correspondientes
- habilitados (ej: MySQL y intl).
-#. Si el host de base de datos es 'localhost', intente realizar la conexión con
- el ip '127.0.0.1'. En algunos casos PHP CLI tiene problemas al referenciar
- por nombre de host (localhost).
-#. Dependiendo de como esté configurado su equipo, la ejecución del comando
- CAKE BAKE (cake bash script) puede requerir permisos de ejecución al
- lanzar ``bin/cake bake``.
-
-Antes de comenzar la ejecución, asegúrese de disponer al menos de una conexión
-a una base de datos configurada.
-
-Para comenzar con la ejecución del comando debe abrir la consola de windows
-y ejecutar "Cake Bake"
-
-#. Ir a Inicio (Start) > Ejecutar (Run)
-#. Escribir "cmd" y presionar 'Enter'
-#. Navegar hasta llegar a la carpeta de instalación de cake
-#. Acceder a la carpeta 'bin'
-#. Escribir 'Cake bake' lo cual deberá devolver un listado con todas las
- tareas/actividades disponibles.
-
-El resultado debería ser algo similar a lo siguiente::
-
- $ bin/cake bake
-
- Welcome to CakePHP v3.1.6 Console
- ---------------------------------------------------------------
- App : src
- Path: /var/www/cakephp.dev/src/
- PHP: 5.5.8
- ---------------------------------------------------------------
- The following commands can be used to generate skeleton code for your application.
-
- Available bake commands:
-
- - all
- - behavior
- - cell
- - component
- - controller
- - fixture
- - form
- - helper
- - mailer
- - migration
- - migration_snapshot
- - model
- - plugin
- - shell
- - shell-helper
- - template
- - test
-
- By using 'cake bake [name]' you can invoke a specific bake task.
-
-Puede obtener más información sobre lo que realiza cada una de las actividades
-y sus opciones usando el parametro '--help' option::
-
- $ bin/cake bake controller --help
-
- Welcome to CakePHP v3.1.6 Console
- ---------------------------------------------------------------
- App : src
- Path: /var/www/cakephp.dev/src/
- ---------------------------------------------------------------
- Bake a controller skeleton.
-
- Usage:
- cake bake controller [subcommand] [options] []
-
- Subcommands:
-
- all Bake all controllers with CRUD methods.
-
- To see help on a subcommand use `cake bake controller [subcommand] --help`
-
- Options:
-
- --help, -h Display this help.
- --verbose, -v Enable verbose output.
- --quiet, -q Enable quiet output.
- --plugin, -p Plugin to bake into.
- --force, -f Force overwriting existing files without prompting.
- --connection, -c The datasource connection to get data from.
- (default: default)
- --theme, -t The theme to use when baking code.
- --components The comma separated list of components to use.
- --helpers The comma separated list of helpers to use.
- --prefix The namespace/routing prefix to use.
- --no-test Do not generate a test skeleton.
- --no-actions Do not generate basic CRUD action methods.
-
- Arguments:
-
- name Name of the controller to bake. Can use Plugin.name to bake
- controllers into plugins. (optional)
-
-Temas Bake / Templates
-======================
-
-La opción ``theme`` es genérica para todos los comandos bake y permite cambiar los
-templates de bake utilizados para generar los archivos finales. Para crear sus
-propios templates, ver :ref:`bake theme creation documentation
-`.
-
-.. meta::
- :title lang=es: Crear código con Bake
- :keywords lang=es: interfaz de línea de comando, aplicación funcional, base de datos, configuración de base de datos, bash script, ingredientes básicos, proyecto, modelo, path, crear código, generación de código, scaffolding, usuarios windows, archivo de configuración, pocos minutos, configurar, iew, shell, modelos, running, mysql
diff --git a/app/vendor/cakephp/bake/docs/fr/conf.py b/app/vendor/cakephp/bake/docs/fr/conf.py
deleted file mode 100644
index b02032efa..000000000
--- a/app/vendor/cakephp/bake/docs/fr/conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import sys, os
-
-# Append the top level directory of the docs, so we can import from the config dir.
-sys.path.insert(0, os.path.abspath('..'))
-
-# Pull in all the configuration options defined in the global config file..
-from config.all import *
-
-language = 'fr'
diff --git a/app/vendor/cakephp/bake/docs/fr/contents.rst b/app/vendor/cakephp/bake/docs/fr/contents.rst
deleted file mode 100644
index 08c3e957c..000000000
--- a/app/vendor/cakephp/bake/docs/fr/contents.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. toctree::
- :maxdepth: 2
- :caption: CakePHP Bake
-
- /index
- /usage
- /development
diff --git a/app/vendor/cakephp/bake/docs/fr/development.md b/app/vendor/cakephp/bake/docs/fr/development.md
new file mode 100644
index 000000000..8d08ce39d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/fr/development.md
@@ -0,0 +1,297 @@
+# Étendre Bake
+
+Bake dispose d'une architecture extensible qui permet à votre application ou à vos plugins de modifier ou d'ajouter des fonctionnalités de base.
+Bake utilise une classe de vue dédiée fondée sur le moteur de template [Twig](https://twig.symfony.com/).
+
+## Events de Bake
+
+Comme une classe de vue, `BakeView` envoie les mêmes events que toute autre classe de vue, ainsi qu'un event `initialize` supplémentaire.
+Alors que les classes de vue standard utilisent le préfixe `View.`, `BakeView` utilise le préfixe `Bake.`.
+
+L'event `initialize` peut être utilisé pour faire des changements qui s'appliquent à toutes les sorties générées par Bake.
+Par exemple, pour ajouter un helper :
+
+```php
+on('Bake.initialize', function (EventInterface $event) {
+ $view = $event->getSubject();
+
+ // Dans mes templates de bake, permet l'utilisation du helper MySpecial
+ $view->loadHelper('MySpecial', ['some' => 'config']);
+
+ // Et ajoute une variable $author pour qu'elle soit toujours disponible
+ $view->set('author', 'Andy');
+});
+```
+
+Les events de Bake peuvent aussi servir à faire de petits changements dans les templates existants.
+Par exemple, pour changer les noms de variables utilisés lors de la création de fichiers de controller et de template :
+
+```php
+on('Bake.beforeRender', function (EventInterface $event) {
+ $view = $event->getSubject();
+
+ // Utilise $rows pour la principale variable de données dans les index
+ if ($view->get('pluralName')) {
+ $view->set('pluralName', 'rows');
+ }
+ if ($view->get('pluralVar')) {
+ $view->set('pluralVar', 'rows');
+ }
+
+ // Utilise $theOne pour la principale variable de données dans les view/edit
+ if ($view->get('singularName')) {
+ $view->set('singularName', 'theOne');
+ }
+ if ($view->get('singularVar')) {
+ $view->set('singularVar', 'theOne');
+ }
+});
+```
+
+Vous pouvez aussi scoper les events `Bake.beforeRender` et `Bake.afterRender` à un fichier généré spécifique.
+Par exemple, pour ajouter des actions à `UsersController` lors de la génération depuis `Controller/controller.twig` :
+
+```php
+on(
+ 'Bake.beforeRender.Controller.controller',
+ function (EventInterface $event) {
+ $view = $event->getSubject();
+ if ($view->get('name') === 'Users') {
+ // ajouter les actions login et logout au controller Users
+ $view->set('actions', [
+ 'login',
+ 'logout',
+ 'index',
+ 'view',
+ 'add',
+ 'edit',
+ 'delete',
+ ]);
+ }
+ }
+);
+```
+
+En scopant les écouteurs d'event vers des templates de Bake spécifiques, vous simplifiez la logique liée à Bake et obtenez des callbacks plus faciles à tester.
+
+## Syntaxe de template de Bake
+
+Les fichiers de template de Bake utilisent la syntaxe [Twig](https://twig.symfony.com/).
+
+Par exemple, si vous générez une commande comme ceci :
+
+```bash
+bin/cake bake command Foo
+```
+
+Le template utilisé dans `vendor/cakephp/bake/templates/bake/Command/command.twig` ressemble à ceci :
+
+```php
+Test->classSuffixes[$this->name()])) {
+ $this->Test->classSuffixes[$this->name()] = 'Foo';
+ }
+
+ $name = ucfirst($this->name());
+ if (!isset($this->Test->classTypes[$name])) {
+ $this->Test->classTypes[$name] = 'Foo';
+ }
+
+ return parent::bakeTest($className);
+}
+```
+
+- Le **suffixe de classe** sera ajouté après le nom passé à `bake`. Dans l'exemple ci-dessus, cela créerait `ExempleFooTest.php`.
+- Le **type de classe** sera le sous-namespace utilisé pour atteindre le fichier relatif à l'application ou au plugin. Dans l'exemple ci-dessus, cela créerait le namespace `App\Test\TestCase\Foo`.
diff --git a/app/vendor/cakephp/bake/docs/fr/development.rst b/app/vendor/cakephp/bake/docs/fr/development.rst
deleted file mode 100644
index b4f72c38d..000000000
--- a/app/vendor/cakephp/bake/docs/fr/development.rst
+++ /dev/null
@@ -1,347 +0,0 @@
-Etendre Bake
-############
-
-Bake dispose d'une architecture extensible qui permet à votre application ou
-à vos plugins de modifier ou ajouter la fonctionnalité de base. Bake utilise une
-classe de vue dédiée qui utilise le moteur de template
-`Twig `_.
-
-Events de Bake
-==============
-
-Comme une classe de vue, ``BakeView`` envoie les mêmes events que toute autre
-classe de vue, ainsi qu'un event initialize supplémentaire. Cependant,
-alors que les classes de vue standard utilisent le préfixe d'event
-"View.", ``BakeView`` utilise le préfixe d'event "Bake.".
-
-L'event initialize peut être utilisé pour faire des changements qui
-s'appliquent à toutes les sorties fabriquées avec bake, par exemple pour ajouter
-un autre helper à la classe de vue bake, cet event peut être utilisé::
-
- on('Bake.initialize', function (EventInterface $event) {
- $view = $event->getSubject();
-
- // Dans mes templates de bake, permet l'utilisation du helper MySpecial
- $view->loadHelper('MySpecial', ['some' => 'config']);
-
- // Et ajoute une variable $author pour qu'elle soit toujours disponible
- $view->set('author', 'Andy');
-
- });
-
-Les events de bake peuvent être pratiques pour faire de petits changements dans
-les templates existants. Par exemple, pour changer les noms de variables
-utilisés lors de la création avec bake de fichiers de controller/template, on
-pourra utiliser une fonction qui écoute ``Bake.beforeRender`` pour modifier les
-variables utilisées dans les templates de bake::
-
- on('Bake.beforeRender', function (EventInterface $event) {
- $view = $event->getSubject();
-
- // Utilise $rows pour la principale variable de données dans les index
- if ($view->get('pluralName')) {
- $view->set('pluralName', 'rows');
- }
- if ($view->get('pluralVar')) {
- $view->set('pluralVar', 'rows');
- }
-
- // Utilise $theOne pour la principale variable de données dans les view/edit
- if ($view->get('singularName')) {
- $view->set('singularName', 'theOne');
- }
- if ($view->get('singularVar')) {
- $view->set('singularVar', 'theOne');
- }
-
- });
-
-Vous pouvez aussi scoper les events ``Bake.beforeRender`` et
-``Bake.afterRender`` dans un fichier généré spécifique. Par exemple, si vous
-souhaitez ajouter des actions spécifiques à votre UsersController quand vous le
-générez à partir d'un fichier **Controller/controller.twig**, vous pouvez
-utiliser l'event suivant::
-
- on(
- 'Bake.beforeRender.Controller.controller',
- function (EventInterface $event) {
- $view = $event->getSubject();
- if ($view->get('name') === 'Users') {
- // ajouter les actions login et logout au controller Users
- $view->set('actions', [
- 'login',
- 'logout',
- 'index',
- 'view',
- 'add',
- 'edit',
- 'delete'
- ];
- }
- }
- );
-
-En scopant les écouteurs d'event vers des templates de bake spécifiques, vous
-pouvez simplifier votre logique d'event liée à bake et fournir des callbacks
-qui sont plus faciles à tester.
-
-Syntaxe de Template de Bake
-===========================
-
-Les fichiers de template de Bake utilisent la syntaxe de template de
-`Twig `__.
-
-Ainsi, par exemple, pour créer avec bake un shell comme ceci:
-
-.. code-block:: bash
-
- bin/cake bake command Foo
-
-Le template utilisé
-(***vendor/cakephp/bake/templates/bake/Command/command.twig**)
-ressemble à ceci::
-
- declare(strict_types=1);
-
- namespace {{ namespace }}\Command;
-
- use Cake\Command\Command;
- use Cake\Console\Arguments;
- use Cake\Console\ConsoleIo;
- use Cake\Console\ConsoleOptionParser;
-
- /**
- * {{ name }} command.
- */
- class {{ name }}Command extends Command
- {
- /**
- * Méthode hook pour définir le parseur d'option de cette commande.
- *
- * @see https://book.cakephp.org/4/fr/console-commands/commands.html#defining-arguments-and-options
- * @param \Cake\Console\ConsoleOptionParser $parser Le parseur à définir
- * @return \Cake\Console\ConsoleOptionParser Le parseur construit.
- */
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
- {
- $parser = parent::buildOptionParser($parser);
-
- return $parser;
- }
-
- /**
- * Implémentez cette méthode avec la logique de votre commande.
- *
- * @param \Cake\Console\Arguments $args Les arguments de la commande.
- * @param \Cake\Console\ConsoleIo $io La console il
- * @return null|void|int Le code de sortie ou null pour un succès
- */
- public function execute(Arguments $args, ConsoleIo $io)
- {
- }
- }
-
-Et la classe résultante construite avec bake (**src/Command/FooCommand.php**)
-ressemble à ceci::
-
- Test->classSuffixes[$this->name()])) {
- $this->Test->classSuffixes[$this->name()] = 'Foo';
- }
-
- $name = ucfirst($this->name());
- if (!isset($this->Test->classTypes[$name])) {
- $this->Test->classTypes[$name] = 'Foo';
- }
-
- return parent::bakeTest($className);
- }
-
-* Le **suffixe de classe** sera ajouté après le nom passé à ``bake``. Dans le
- cadre de l'exemple ci-dessus, cela créerait un fichier ``ExempleFooTest.php``.
-* Le **type de classe** sera le sous-namespace utilisé pour atteindre votre
- fichier (relatif à l'application ou au plugin dans lequel vous faites le
- ``bake``). Dans le cadre de l'exemple ci-dessus, cela créerait le test avec le
- namespace ``App\Test\TestCase\Foo``.
-
-.. meta::
- :title lang=fr: Etendre Bake
- :keywords lang=fr: interface ligne de commande,development,bake view, bake template syntax,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/fr/index.md b/app/vendor/cakephp/bake/docs/fr/index.md
new file mode 100644
index 000000000..07930646f
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/fr/index.md
@@ -0,0 +1,25 @@
+# Console Bake
+
+La console Bake de CakePHP permet de lancer rapidement une application CakePHP.
+Elle peut créer les éléments de base de CakePHP, comme les models, behaviors, views,
+helpers, controllers, components, cas de tests, fixtures et plugins.
+Bake peut aller bien au-delà des classes squelettes et constitue une étape naturelle après un premier prototypage.
+
+## Installation
+
+Avant d'utiliser ou d'étendre Bake, assurez-vous qu'il est installé dans votre application.
+Bake est disponible sous forme de plugin que vous pouvez installer avec Composer :
+
+```bash
+composer require --dev cakephp/bake:"^2.0"
+```
+
+Cela installe Bake comme dépendance de développement, et il ne sera donc pas déployé en production.
+
+Quand vous utilisez les templates Twig, vérifiez que vous chargez le plugin `Cake/TwigView` avec son bootstrap.
+Vous pouvez aussi l'omettre complètement, ce qui fera charger ce plugin à la demande par Bake.
+
+## Plan de la documentation
+
+- [Génération de code avec Bake](/fr/usage) couvre l'exécution du CLI, les commandes disponibles et les thèmes Bake.
+- [Étendre Bake](/fr/development) couvre les events, les templates Twig, les thèmes et les commandes Bake personnalisées.
diff --git a/app/vendor/cakephp/bake/docs/fr/index.rst b/app/vendor/cakephp/bake/docs/fr/index.rst
deleted file mode 100644
index 80e39229b..000000000
--- a/app/vendor/cakephp/bake/docs/fr/index.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-Console Bake
-############
-
-La console Bake de CakePHP est un autre outil permettant de réaliser son
-application rapidement. La console Bake peut créer chacun des ingrédients
-basiques de CakePHP : models, behaviors, views, helpers, controllers,
-components, cas de tests, fixtures et plugins. Et nous ne parlons pas
-seulement des squelettes de classes : Bake peut créer une application
-fonctionnelle complète en seulement quelques minutes. En réalité, Bake est une
-étape naturelle à suivre une fois qu'une application a été prototypée.
-
-Installation
-============
-
-Avant d'essayer d'utiliser ou d'étendre bake, assurez-vous qu'il est installé
-dans votre application. Bake est disponible en tant que plugin que vous pouvez
-installer avec Composer::
-
- composer require --dev cakephp/bake:"^2.0"
-
-Ceci va installer bake en tant que dépendance de développement. Cela signifie
-qu'il ne sera pas installé lors d'un déploiement en production.
-
-Quand vous utilisez les templates Twig, vérifiez que vous chargez le plugin
-``Cake/TwigView`` avec son bootstrap. Vous pouvez aussi l'omettre complètement,
-ce qui fait que Bake chargera ce plugin à la demande.
-
-.. meta::
- :title lang=fr: Console Bake
- :keywords lang=fr: interface ligne de commande,development,bake view, bake template syntaxe,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/fr/usage.md b/app/vendor/cakephp/bake/docs/fr/usage.md
new file mode 100644
index 000000000..3515cd8bf
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/fr/usage.md
@@ -0,0 +1,53 @@
+# Génération de code avec Bake
+
+La console Bake est exécutée avec le CLI PHP.
+Si vous avez des problèmes en exécutant ce script, vérifiez que :
+
+1. Le CLI PHP est installé et qu'il a les bons modules activés, par exemple MySQL et `intl`.
+2. Si l'hôte de la base de données est `localhost`, essayez `127.0.0.1`, car `localhost` peut causer des problèmes avec PHP CLI.
+3. Selon la configuration de votre ordinateur, vous devrez peut-être donner les permissions d'exécution au script `cake` pour autoriser le lancement via `bin/cake bake`.
+
+Avant de lancer Bake, vous devez vous assurer qu'au moins une connexion de base de données est configurée.
+
+Vous pouvez voir la liste des commandes disponibles en lançant `bin/cake bake --help`.
+Pour Windows, utilisez `bin\cake bake --help` :
+
+```bash
+$ bin/cake bake --help
+Current Paths:
+
+* app: src/
+* root: /path/to/your/app/
+* core: /path/to/your/app/vendor/cakephp/cakephp/
+
+Available Commands:
+
+Bake:
+- bake all
+- bake behavior
+- bake cell
+- bake command
+- bake component
+- bake controller
+- bake controller all
+- bake fixture
+- bake fixture all
+- bake form
+- bake helper
+- bake mailer
+- bake middleware
+- bake model
+- bake model all
+- bake plugin
+- bake template
+- bake template all
+- bake test
+
+To run a command, type `cake command_name [args|options]`
+To get help on a specific command, type `cake command_name --help`
+```
+
+## Thèmes de Bake
+
+L'option `theme` est commune à toutes les commandes Bake et permet de changer les fichiers de template utilisés lors de la génération.
+Pour créer vos propres templates, référez-vous à [Créer un thème de Bake](/fr/development#creer-un-theme-de-bake).
diff --git a/app/vendor/cakephp/bake/docs/fr/usage.rst b/app/vendor/cakephp/bake/docs/fr/usage.rst
deleted file mode 100644
index 79f9f4957..000000000
--- a/app/vendor/cakephp/bake/docs/fr/usage.rst
+++ /dev/null
@@ -1,65 +0,0 @@
-Génération de Code avec Bake
-############################
-
-La console Bake est exécutée en utilisant le CLI PHP
-(Interface de Ligne de Commande). Si vous avez des problèmes en exécutant ce
-script, vérifiez que :
-
-#. le CLI PHP est installé et qu'il a les bons modules activés (ex: MySQL, intl).
-#. Certains utilisateurs peuvent aussi rencontrer des problèmes si l'hôte de la
- base de données est 'localhost' et devront essayer '127.0.0.1' à la place,
- car localhost peut causer des problèmes avec PHP CLI.
-#. Selon la configuration de votre ordinateur, vous devrez peut-être donner les
- permissions d'exécution sur le script bash cake pour autoriser le lancement
- par ``bin/cake bake``.
-
-Avant de lancer bake, vous devrez vous assurer que vous avez au moins une
-connexion de base de données configurée.
-
-Vous pouvez voir la liste des commandes bake disponibles en lançant
-``bin/cake bake --help`` (pour Windows ``bin\cake bake --help``) ::
-
- $ bin/cake bake --help
- Current Paths:
-
- * app: src/
- * root: /path/to/your/app/
- * core: /path/to/your/app/vendor/cakephp/cakephp/
-
- Available Commands:
-
- Bake:
- - bake all
- - bake behavior
- - bake cell
- - bake command
- - bake component
- - bake controller
- - bake controller all
- - bake fixture
- - bake fixture all
- - bake form
- - bake helper
- - bake mailer
- - bake middleware
- - bake model
- - bake model all
- - bake plugin
- - bake template
- - bake template all
- - bake test
-
- To run a command, type `cake command_name [args|options]`
- To get help on a specific command, type `cake command_name --help`
-
-Thèmes de Bake
-==============
-
-L'option theme est commune à toutes les commandes de bake, et permet de changer
-les fichiers de template utilisés lors de la création avec bake. Pour créer vos
-propres templates, référez-vous :ref:`à la documentation sur la création de
-theme bake `.
-
-.. meta::
- :title lang=fr: Génération de Code avec Bake
- :keywords lang=fr: interface ligne de commande,application fonctionnelle,base de données,configuration base de données,bash script,ingredients basiques,project,model,path path,génération de code,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql
diff --git a/app/vendor/cakephp/bake/docs/ja/conf.py b/app/vendor/cakephp/bake/docs/ja/conf.py
deleted file mode 100644
index 5871da648..000000000
--- a/app/vendor/cakephp/bake/docs/ja/conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import sys, os
-
-# Append the top level directory of the docs, so we can import from the config dir.
-sys.path.insert(0, os.path.abspath('..'))
-
-# Pull in all the configuration options defined in the global config file..
-from config.all import *
-
-language = 'ja'
diff --git a/app/vendor/cakephp/bake/docs/ja/contents.rst b/app/vendor/cakephp/bake/docs/ja/contents.rst
deleted file mode 100644
index 08c3e957c..000000000
--- a/app/vendor/cakephp/bake/docs/ja/contents.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. toctree::
- :maxdepth: 2
- :caption: CakePHP Bake
-
- /index
- /usage
- /development
diff --git a/app/vendor/cakephp/bake/docs/ja/development.md b/app/vendor/cakephp/bake/docs/ja/development.md
new file mode 100644
index 000000000..867c3d313
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/ja/development.md
@@ -0,0 +1,315 @@
+# Bake の拡張
+
+Bake は、アプリケーションやプラグインが基本機能を変更または追加できる拡張可能なアーキテクチャーを備えています。
+Bake は [Twig](https://twig.symfony.com/) テンプレートエンジンを使う専用のビュークラスを利用します。
+
+## Bake イベント
+
+`BakeView` は、他のビュークラスと同様のイベントに加え、特別な initialize イベントを発します。
+標準ビュークラスはイベントプレフィックス `View.` を使いますが、`BakeView` は `Bake.` を使います。
+
+initialize イベントは、すべての Bake 出力に対する変更に使用できます。
+たとえば、Bake ビュークラスに別のヘルパーを追加するには次のようにします。
+
+```php
+on('Bake.initialize', function (EventInterface $event) {
+ $view = $event->getSubject();
+
+ // bake テンプレートの中で MySpecial ヘルパーの使用を可能にします
+ $view->loadHelper('MySpecial', ['some' => 'config']);
+
+ // そして、$author 変数を利用可能にするために追加
+ $view->set('author', 'Andy');
+});
+```
+
+別のプラグインの中から Bake を変更したい場合は、プラグインの `config/bootstrap.php` に Bake イベントを置くのが有効です。
+
+Bake イベントは、既存テンプレートへの小さな変更にも役立ちます。
+たとえば、コントローラーやテンプレートファイルを Bake するときに使う変数名を変更するには `Bake.beforeRender` を利用します。
+
+```php
+on('Bake.beforeRender', function (EventInterface $event) {
+ $view = $event->getSubject();
+
+ // indexes の中のメインデータ変数に $rows を使用
+ if ($view->get('pluralName')) {
+ $view->set('pluralName', 'rows');
+ }
+ if ($view->get('pluralVar')) {
+ $view->set('pluralVar', 'rows');
+ }
+
+ // view と edit の中のメインデータ変数に $theOne を使用
+ if ($view->get('singularName')) {
+ $view->set('singularName', 'theOne');
+ }
+ if ($view->get('singularVar')) {
+ $view->set('singularVar', 'theOne');
+ }
+});
+```
+
+`Bake.beforeRender` と `Bake.afterRender` を特定の生成ファイルに限定することもできます。
+たとえば `Controller/controller.twig` から生成する際に `UsersController` に特定アクションを追加したい場合は次のイベントを使用できます。
+
+```php
+on(
+ 'Bake.beforeRender.Controller.controller',
+ function (EventInterface $event) {
+ $view = $event->getSubject();
+ if ($view->get('name') === 'Users') {
+ // Users コントローラーに login と logout を追加
+ $view->set('actions', [
+ 'login',
+ 'logout',
+ 'index',
+ 'view',
+ 'add',
+ 'edit',
+ 'delete',
+ ]);
+ }
+ }
+);
+```
+
+特定の Bake テンプレートにイベントリスナーを絞ることで、Bake 関連のイベントロジックを単純にし、テストしやすいコールバックを提供できます。
+
+## Bake テンプレート構文
+
+Bake テンプレートファイルは [Twig](https://twig.symfony.com/doc/2.x/) 構文を使用します。
+
+たとえば、次のようにコマンドを Bake した場合:
+
+```bash
+bin/cake bake command Foo
+```
+
+`vendor/cakephp/bake/templates/bake/Command/command.twig` のテンプレートは次のようになります。
+
+```php
+Test->classSuffixes[$this->name()])) {
+ $this->Test->classSuffixes[$this->name()] = 'Foo';
+ }
+
+ $name = ucfirst($this->name());
+ if (!isset($this->Test->classTypes[$name])) {
+ $this->Test->classTypes[$name] = 'Foo';
+ }
+
+ return parent::bakeTest($className);
+}
+```
+
+- **class suffix** は `bake` 呼び出しで与えた名前に追加されます。前の例では `ExampleFooTest.php` を作成します。
+- **class type** はファイルに到達するためのサブ名前空間です。前の例では `App\Test\TestCase\Foo` という名前空間でテストを作成します。
+
+## BakeView クラスの設定
+
+Bake コマンドは、テンプレートをレンダリングするために `BakeView` クラスを使います。
+`Bake.initialize` イベントを監視するとインスタンスにアクセスできます。
+
+```php
+on(
+ 'Bake.initialize',
+ function ($event, $view) {
+ $view->loadHelper('Foo');
+ }
+);
+```
diff --git a/app/vendor/cakephp/bake/docs/ja/development.rst b/app/vendor/cakephp/bake/docs/ja/development.rst
deleted file mode 100644
index 070e43a5c..000000000
--- a/app/vendor/cakephp/bake/docs/ja/development.rst
+++ /dev/null
@@ -1,296 +0,0 @@
-Bake の拡張
-###########
-
-Bake は、アプリケーションやプラグインが基本機能に対して変更または追加を可能にする
-拡張可能なアーキテクチャーを備えています。Bake は、 `Twig `_
-テンプレートエンジンを使用したビュークラスを利用します。
-
-Bake イベント
-=============
-
-``BakeView`` は、ビュークラスとして、他のビュークラスと同様のイベントに加え、
-1つの特別な初期化 (initialize) イベントを発します。しかし、一方で標準ビュークラスは、
-イベントのプレフィックス "View." を使用しますが、 ``BakeView`` は、
-イベントのプレフィックス "Bake." を使用しています。
-
-初期化イベントは、すべての bake の出力に対して変更を加えるために使用できます。
-例えば、bake ビュークラスに他のヘルパーを追加するためにこのイベントは使用されます。 ::
-
- on('Bake.initialize', function (Event $event) {
- $view = $event->getSubject();
-
- // bake テンプレートの中で MySpecial ヘルパーの使用を可能にします
- $view->loadHelper('MySpecial', ['some' => 'config']);
-
- // そして、$author 変数を利用可能にするために追加
- $view->set('author', 'Andy');
-
- });
-
-別のプラグインの中から bake を変更したい場合は、プラグインの ``config/bootstrap.php``
-ファイルでプラグインの Bake イベントを置くことは良いアイデアです。
-
-Bake イベントは、既存のテンプレートに小さな変更を行うための便利なことができます。
-例えば、コントローラーやテンプレートファイルを bake する際に使用される変数名を
-変更するために、bake テンプレートで使用される変数を変更するために
-``Bake.beforeRender`` で呼び出される関数を使用することができます。 ::
-
- on('Bake.beforeRender', function (Event $event) {
- $view = $event->getSubject();
-
- // indexes の中のメインデータ変数に $rows を使用
- if ($view->get('pluralName')) {
- $view->set('pluralName', 'rows');
- }
- if ($view->get('pluralVar')) {
- $view->set('pluralVar', 'rows');
- }
-
- // view と edit の中のメインデータ変数に $theOne を使用
- if ($view->get('singularName')) {
- $view->set('singularName', 'theOne');
- }
- if ($view->get('singularVar')) {
- $view->set('singularVar', 'theOne');
- }
-
- });
-
-特定の生成されたファイルへの ``Bake.beforeRender`` と ``Bake.afterRender``
-イベントを指定することもあるでしょう。例えば、
-**Controller/controller.twig** ファイルから生成する際、 UsersController
-に特定のアクションを追加したい場合、以下のイベントを使用することができます。 ::
-
- on(
- 'Bake.beforeRender.Controller.controller',
- function (Event $event) {
- $view = $event->getSubject();
- if ($view->viewVars['name'] == 'Users') {
- // Users コントローラーに login と logout を追加
- $view->viewVars['actions'] = [
- 'login',
- 'logout',
- 'index',
- 'view',
- 'add',
- 'edit',
- 'delete'
- ];
- }
- }
- );
-
-特定の bake テンプレートのためのイベントリスナーを指定することによって、
-bake 関連のイベント・ロジックを簡素化し、テストするのが容易であるコールバックを
-提供することができます。
-
-Bake テンプレート構文
-=====================
-
-Bake テンプレートファイルは、 `Twig `__
-テンプレート構文を使用します。
-
-だから、例えば、以下のようにシェルを bake した場合:
-
-.. code-block:: bash
-
- bin/cake bake shell Foo
-
-(**vendor/cakephp/bake/src/Template/Bake/Shell/shell.twig**) を使用した
-テンプレートは、以下のようになります。 ::
-
- `` Bake テンプレートの PHP 終了タグ
- * ``<%=`` Bake テンプレートの PHP ショートエコータグ
- * ``<%-`` Bake テンプレートの PHP 開始タグ、タグの前に、先頭の空白を除去
- * ``-%>`` Bake テンプレートの PHP 終了タグ、タグの後に末尾の空白を除去
-
-.. _creating-a-bake-theme:
-
-Bake テーマの作成
-=================
-
-"bake" コマンドによって生成された出力を変更したい場合、bake が使用するテンプレートの
-一部または全部を置き換えることができる、独自の bake の「テーマ」を作成することができます。
-これを行うための最善の方法は、次のとおりです。
-
-#. 新しいプラグインを bake します。プラグインの名前は bake の「テーマ」名になります。
-#. 新しいディレクトリー **plugins/[name]/src/Template/Bake/Template/** を作成します。
-#. **vendor/cakephp/bake/src/Template/Bake/Template** から上書きしたい
- テンプレートをあなたのプラグインの中の適切なファイルにコピーしてください。
-#. bake を実行するときに、必要であれば、 bake のテーマを指定するための ``--theme``
- オプションを使用してください。各呼び出しでこのオプションを指定しなくても済むように、
- カスタムテーマをデフォルトテーマとして使用するように設定することもできます。 ::
-
- Test->classSuffixes[$this->name()])) {
- $this->Test->classSuffixes[$this->name()] = 'Foo';
- }
-
- $name = ucfirst($this->name());
- if (!isset($this->Test->classTypes[$name])) {
- $this->Test->classTypes[$name] = 'Foo';
- }
-
- return parent::bakeTest($className);
- }
-
-* **class suffix** は ``bake`` 呼び出しで与えられた名前に追加します。前の例では、
- ``ExampleFooTest.php`` ファイルを作成します。
-* **class type** は、(あなたが bake するアプリやプラグインに関連する)
- あなたのファイルを導くために使用されるサブ名前空間です。
- 前の例では、名前空間 ``App\Test\TestCase\Foo`` でテストを作成します。
-
-.. meta::
- :title lang=ja: Bake の拡張
- :keywords lang=ja: command line interface,development,bake view, bake template syntax,twig,erb tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/ja/index.md b/app/vendor/cakephp/bake/docs/ja/index.md
new file mode 100644
index 000000000..bad397c78
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/ja/index.md
@@ -0,0 +1,26 @@
+# Bake コンソール
+
+CakePHP の Bake コンソールは、CakePHP を素早く使い始めるためのツールです。
+Bake コンソールは、モデル、ビヘイビアー、ビュー、ヘルパー、コントローラー、コンポーネント、
+テストケース、フィクスチャー、プラグインなど、CakePHP の基本的な素材を作成できます。
+単なるスケルトンクラスだけではなく、数分で完全に機能するアプリケーションを作成できます。
+
+## インストール手順
+
+Bake を使用したり拡張する前に、アプリケーションに Bake をインストールしてください。
+Bake は Composer を使ってインストールするプラグインとして提供されています。
+
+```bash
+composer require --dev cakephp/bake:"^3.0"
+```
+
+このコマンドは、Bake を開発用依存パッケージとしてインストールします。
+そのため、本番環境へのデプロイ時にはインストールされません。
+
+Twig テンプレートを使用する場合、`Cake/TwigView` プラグインをブートストラップとともに読み込んでいることを確認してください。
+完全に省略し、Bake プラグインに必要時だけ読み込ませることもできます。
+
+## ドキュメント一覧
+
+- [Bake でコード生成](/ja/usage) では CLI の実行、利用可能なコマンド、モデル生成、テーマについて説明します。
+- [Bake の拡張](/ja/development) ではイベント、Twig テンプレート、テーマ、カスタム Bake コマンドについて説明します。
diff --git a/app/vendor/cakephp/bake/docs/ja/index.rst b/app/vendor/cakephp/bake/docs/ja/index.rst
deleted file mode 100644
index a4e503963..000000000
--- a/app/vendor/cakephp/bake/docs/ja/index.rst
+++ /dev/null
@@ -1,28 +0,0 @@
-Bake コンソール
-################
-
-CakePHP の bake コンソールは、迅速に CakePHP を動作させるまでを支援します。
-bake コンソールは、CakePHP の基本的な素材(モデル、ビヘイビアー、ビュー、ヘルパー、
-コントローラー、コンポーネント、テストケース、フィクスチャー、プラグイン)を作成できます。
-その為のスケルトンクラスについては、ここでは省略しますが、
-bake は数分で完全に機能するアプリケーションを作成できます。
-要するに、bake は足場の組まれたアプリケーションをいっぺんに手に入れるためにうってつけの方法です。
-
-インストール手順
-=================
-
-bake を使用したり拡張する前に、アプリケーションに bake をインストールしておいてください。
-bake は Composer を使ってインストールするプラグインとして提供されています。 ::
-
- composer require --dev cakephp/bake:"^2.0"
-
-上記のコマンドは、bake を開発環境で使用するパッケージとしてインストールします。
-この入れ方の場合、本番環境としてデプロイする際には、 bake はインストールされません。
-
-Twig テンプレートを使用する場合、 ``Cake/TwigView`` プラグインをブートストラップとともに
-読み込んでいることを確認してください。それを完全に省略して、
-Bake プラグインにこのプラグインを読み込ませることもできます。
-
-.. meta::
- :title lang=ja: Bakeコンソール
- :keywords lang=ja: コマンドライン,CLI,development,bake view, bake template syntax,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/ja/usage.md b/app/vendor/cakephp/bake/docs/ja/usage.md
new file mode 100644
index 000000000..09f3ec27d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/ja/usage.md
@@ -0,0 +1,68 @@
+# Bake でコード生成
+
+Bake コンソールは PHP CLI で実行します。
+スクリプトの実行に問題がある場合は、次を確認してください。
+
+1. PHP CLI がインストールされていて、必要なモジュールが有効になっていること。例: MySQL、`intl`。
+2. データベースホストが `localhost` の場合は、代わりに `127.0.0.1` を試すこと。PHP CLI で問題になることがあります。
+3. コンピューターの設定によっては、`bin/cake bake` で使用する Cake スクリプトに実行権限を付ける必要があること。
+
+Bake を実行する前に、少なくとも 1 つのデータベース接続が設定されていることを確認してください。
+
+`bin/cake bake --help` を実行すると、利用可能な Bake コマンドを表示できます。
+Windows では `bin\cake bake --help` を使用します。
+
+```bash
+$ bin/cake bake --help
+Current Paths:
+
+* app: src/
+* root: /path/to/your/app/
+* core: /path/to/your/app/vendor/cakephp/cakephp/
+
+Available Commands:
+
+Bake:
+- bake all
+- bake behavior
+- bake cell
+- bake command
+- bake command_helper
+- bake component
+- bake controller
+- bake controller all
+- bake enum
+- bake fixture
+- bake fixture all
+- bake form
+- bake helper
+- bake mailer
+- bake middleware
+- bake model
+- bake model all
+- bake plugin
+- bake template
+- bake template all
+- bake test
+
+To run a command, type `cake command_name [args|options]`
+To get help on a specific command, type `cake command_name --help`
+```
+
+## Bake モデル
+
+モデルは既存のデータベーステーブルから生成されます。
+規約が適用されるため、外部キー `thing_id` とテーブル `things` の主キー `id` に基づいてリレーションが検出されます。
+
+規約から外れたリレーションの場合は、制約や外部キー定義の参照を使って Bake にリレーションを検出させることができます。
+
+```php
+->addForeignKey('billing_country_id', 'countries') // defaults to `id`
+->addForeignKey('shipping_country_id', 'countries', 'cid')
+```
+
+## Bake テーマ
+
+テーマオプションはすべての Bake コマンドで共通です。
+Bake 時に使用するテンプレートファイルを変更できます。
+テーマを作るには、[Bake テーマの作成](/ja/development#bake-テーマの作成) を参照してください。
diff --git a/app/vendor/cakephp/bake/docs/ja/usage.rst b/app/vendor/cakephp/bake/docs/ja/usage.rst
deleted file mode 100644
index d9470e6c3..000000000
--- a/app/vendor/cakephp/bake/docs/ja/usage.rst
+++ /dev/null
@@ -1,102 +0,0 @@
-Bake でコード生成
-##################
-
-cake コンソールは、 PHP CLI (command line interface) で実行します。
-もしスクリプトの実行に問題があるなら、以下を満たしてください。
-
-#. PHP CLI がインストールされているか適切なモジュールが有効か確認してください (例:MySQL, intl)。
-#. データベースのホストが 'localhost' で問題があるなら、代わりに '127.0.0.1' を使って下さい。
- PHP CLI でこの問題がおこる可能性があります。
-#. 使っているコンピューターの設定に応じて、 ``bin/cake bake`` で使用する cake bash スクリプトの
- 実行権限を設定する必要があります。
-
-bake を実行する前にデータベースとの接続を確認しましょう。
-
-``bin/cake bake`` を引数無しで実行すると可能なタスクを表示できます。
-
-Windows システムの場合、 ``bin\cake bake`` を試してみてください。
-
-それは以下のように表示されます。 ::
-
- $ bin/cake bake
-
- Welcome to CakePHP v3.1.6 Console
- ---------------------------------------------------------------
- App : src
- Path: /var/www/cakephp.dev/src/
- PHP: 5.5.8
- ---------------------------------------------------------------
- The following commands can be used to generate skeleton code for your application.
-
- Available bake commands:
-
- - all
- - behavior
- - cell
- - component
- - controller
- - fixture
- - form
- - helper
- - mailer
- - migration
- - migration_snapshot
- - model
- - plugin
- - shell
- - shell-helper
- - template
- - test
-
- By using `cake bake [name]` you can invoke a specific bake task.
-
-より詳しい各コマンドの情報を得るには、 ``--help`` オプションをつけ実行してください。 ::
-
- $ bin/cake bake controller --help
-
- Welcome to CakePHP v3.1.6 Console
- ---------------------------------------------------------------
- App : src
- Path: /var/www/cakephp.dev/src/
- ---------------------------------------------------------------
- Bake a controller skeleton.
-
- Usage:
- cake bake controller [subcommand] [options] []
-
- Subcommands:
-
- all Bake all controllers with CRUD methods.
-
- To see help on a subcommand use `cake bake controller [subcommand] --help`
-
- Options:
-
- --help, -h Display this help.
- --verbose, -v Enable verbose output.
- --quiet, -q Enable quiet output.
- --plugin, -p Plugin to bake into.
- --force, -f Force overwriting existing files without prompting.
- --connection, -c The datasource connection to get data from.
- (default: default)
- --theme, -t The theme to use when baking code.
- --components The comma separated list of components to use.
- --helpers The comma separated list of helpers to use.
- --prefix The namespace/routing prefix to use.
- --no-test Do not generate a test skeleton.
- --no-actions Do not generate basic CRUD action methods.
-
- Arguments:
-
- name Name of the controller to bake. Can use Plugin.name to bake
- controllers into plugins. (optional)
-
-Bake テーマオプション
-=====================
-
-テーマオプションは全 bake コマンドで一般的です。また、bake テンプレートファイルを変更することができます。
-テーマを作るには、 :ref:`Bake テーマ作成ドキュメント ` をご覧ください。
-
-.. meta::
- :title lang=ja: Code Generation with Bake
- :keywords lang=ja: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql
diff --git a/app/vendor/cakephp/bake/docs/package-lock.json b/app/vendor/cakephp/bake/docs/package-lock.json
new file mode 100644
index 000000000..8236c3b3d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/package-lock.json
@@ -0,0 +1,2099 @@
+{
+ "name": "docs",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "docs",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@cakephp/docs-skeleton": "https://github.com/cakephp/docs-skeleton.git#node-package",
+ "vitepress": "^2.0.0-alpha.16"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@cakephp/docs-skeleton": {
+ "version": "1.0.0",
+ "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#9501c6d47a80645604fceb31328368eeb6a21bc6",
+ "bin": {
+ "cakedocs": "bin/cakedocs.js"
+ },
+ "peerDependencies": {
+ "vitepress": "^2.0.0-alpha.15"
+ }
+ },
+ "node_modules/@docsearch/css": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.2.tgz",
+ "integrity": "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==",
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/js": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.2.tgz",
+ "integrity": "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==",
+ "license": "MIT"
+ },
+ "node_modules/@docsearch/sidepanel-js": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.2.tgz",
+ "integrity": "sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==",
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@iconify-json/simple-icons": {
+ "version": "1.2.76",
+ "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.76.tgz",
+ "integrity": "sha512-lLRlA8yaf+1L5VCPRvR9lynoSklsddKHEylchmZJKdj/q2xVQ1ZAEJ8SCQlv9cbgtMefnlyM98U+8Si2aoFZPA==",
+ "license": "CC0-1.0",
+ "dependencies": {
+ "@iconify/types": "*"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+ "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@shikijs/core": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz",
+ "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.23.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4",
+ "hast-util-to-html": "^9.0.5"
+ }
+ },
+ "node_modules/@shikijs/engine-javascript": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz",
+ "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.23.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "oniguruma-to-es": "^4.3.4"
+ }
+ },
+ "node_modules/@shikijs/engine-oniguruma": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz",
+ "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.23.0",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ }
+ },
+ "node_modules/@shikijs/langs": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz",
+ "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.23.0"
+ }
+ },
+ "node_modules/@shikijs/themes": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz",
+ "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/types": "3.23.0"
+ }
+ },
+ "node_modules/@shikijs/transformers": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz",
+ "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "3.23.0",
+ "@shikijs/types": "3.23.0"
+ }
+ },
+ "node_modules/@shikijs/types": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz",
+ "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/@shikijs/vscode-textmate": {
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "license": "ISC"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
+ "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.2"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
+ "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/shared": "3.5.32",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
+ "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.32",
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
+ "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.2",
+ "@vue/compiler-core": "3.5.32",
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/compiler-ssr": "3.5.32",
+ "@vue/shared": "3.5.32",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.8",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
+ "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz",
+ "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^8.1.1"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz",
+ "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^8.1.1",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "perfect-debounce": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz",
+ "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
+ "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
+ "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.32",
+ "@vue/shared": "3.5.32"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
+ "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.32",
+ "@vue/runtime-core": "3.5.32",
+ "@vue/shared": "3.5.32",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
+ "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.32",
+ "@vue/shared": "3.5.32"
+ },
+ "peerDependencies": {
+ "vue": "3.5.32"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
+ "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
+ "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.2.1",
+ "@vueuse/shared": "14.2.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/integrations": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz",
+ "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vueuse/core": "14.2.1",
+ "@vueuse/shared": "14.2.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "async-validator": "^4",
+ "axios": "^1",
+ "change-case": "^5",
+ "drauu": "^0.4",
+ "focus-trap": "^7 || ^8",
+ "fuse.js": "^7",
+ "idb-keyval": "^6",
+ "jwt-decode": "^4",
+ "nprogress": "^0.2",
+ "qrcode": "^1.5",
+ "sortablejs": "^1",
+ "universal-cookie": "^7 || ^8",
+ "vue": "^3.5.0"
+ },
+ "peerDependenciesMeta": {
+ "async-validator": {
+ "optional": true
+ },
+ "axios": {
+ "optional": true
+ },
+ "change-case": {
+ "optional": true
+ },
+ "drauu": {
+ "optional": true
+ },
+ "focus-trap": {
+ "optional": true
+ },
+ "fuse.js": {
+ "optional": true
+ },
+ "idb-keyval": {
+ "optional": true
+ },
+ "jwt-decode": {
+ "optional": true
+ },
+ "nprogress": {
+ "optional": true
+ },
+ "qrcode": {
+ "optional": true
+ },
+ "sortablejs": {
+ "optional": true
+ },
+ "universal-cookie": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz",
+ "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.2.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz",
+ "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/focus-trap": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.0.1.tgz",
+ "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==",
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.4.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/hast-util-to-html": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "html-void-elements": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "stringify-entities": "^4.0.0",
+ "zwitch": "^2.0.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mark.js": {
+ "version": "8.11.1",
+ "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
+ "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==",
+ "license": "MIT"
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/minisearch": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz",
+ "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/oniguruma-parser": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
+ "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
+ "license": "MIT"
+ },
+ "node_modules/oniguruma-to-es": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
+ "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "oniguruma-parser": "^0.12.1",
+ "regex": "^6.1.0",
+ "regex-recursion": "^6.0.2"
+ }
+ },
+ "node_modules/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-recursion": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
+ "license": "MIT",
+ "dependencies": {
+ "regex-utilities": "^2.3.0"
+ }
+ },
+ "node_modules/regex-utilities": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/shiki": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz",
+ "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@shikijs/core": "3.23.0",
+ "@shikijs/engine-javascript": "3.23.0",
+ "@shikijs/engine-oniguruma": "3.23.0",
+ "@shikijs/langs": "3.23.0",
+ "@shikijs/themes": "3.23.0",
+ "@shikijs/types": "3.23.0",
+ "@shikijs/vscode-textmate": "^10.0.2",
+ "@types/hast": "^3.0.4"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
+ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
+ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitepress": {
+ "version": "2.0.0-alpha.17",
+ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz",
+ "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@docsearch/css": "^4.5.3",
+ "@docsearch/js": "^4.5.3",
+ "@docsearch/sidepanel-js": "^4.5.3",
+ "@iconify-json/simple-icons": "^1.2.69",
+ "@shikijs/core": "^3.22.0",
+ "@shikijs/transformers": "^3.22.0",
+ "@shikijs/types": "^3.22.0",
+ "@types/markdown-it": "^14.1.2",
+ "@vitejs/plugin-vue": "^6.0.4",
+ "@vue/devtools-api": "^8.0.5",
+ "@vue/shared": "^3.5.27",
+ "@vueuse/core": "^14.2.0",
+ "@vueuse/integrations": "^14.2.0",
+ "focus-trap": "^8.0.0",
+ "mark.js": "8.11.1",
+ "minisearch": "^7.2.0",
+ "shiki": "^3.22.0",
+ "vite": "^7.3.1",
+ "vue": "^3.5.27"
+ },
+ "bin": {
+ "vitepress": "bin/vitepress.js"
+ },
+ "peerDependencies": {
+ "markdown-it-mathjax3": "^4",
+ "oxc-minify": "*",
+ "postcss": "^8"
+ },
+ "peerDependenciesMeta": {
+ "markdown-it-mathjax3": {
+ "optional": true
+ },
+ "oxc-minify": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
+ "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/compiler-sfc": "3.5.32",
+ "@vue/runtime-dom": "3.5.32",
+ "@vue/server-renderer": "3.5.32",
+ "@vue/shared": "3.5.32"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ }
+ }
+}
diff --git a/app/vendor/cakephp/bake/docs/package.json b/app/vendor/cakephp/bake/docs/package.json
new file mode 100644
index 000000000..f1407730a
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "docs",
+ "version": "1.0.0",
+ "description": "",
+ "main": "config.js",
+ "scripts": {
+ "docs:dev": "vitepress dev",
+ "docs:build": "vitepress build",
+ "docs:preview": "vitepress preview"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "module",
+ "dependencies": {
+ "@cakephp/docs-skeleton": "git+ssh://git@github.com:cakephp/docs-skeleton.git#node-package",
+ "vitepress": "^2.0.0-alpha.16"
+ }
+}
diff --git a/app/vendor/cakephp/bake/docs/pt/conf.py b/app/vendor/cakephp/bake/docs/pt/conf.py
deleted file mode 100644
index 9e22cb017..000000000
--- a/app/vendor/cakephp/bake/docs/pt/conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import sys, os
-
-# Append the top level directory of the docs, so we can import from the config dir.
-sys.path.insert(0, os.path.abspath('..'))
-
-# Pull in all the configuration options defined in the global config file..
-from config.all import *
-
-language = 'pt'
diff --git a/app/vendor/cakephp/bake/docs/pt/contents.rst b/app/vendor/cakephp/bake/docs/pt/contents.rst
deleted file mode 100644
index 08c3e957c..000000000
--- a/app/vendor/cakephp/bake/docs/pt/contents.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. toctree::
- :maxdepth: 2
- :caption: CakePHP Bake
-
- /index
- /usage
- /development
diff --git a/app/vendor/cakephp/bake/docs/pt/development.md b/app/vendor/cakephp/bake/docs/pt/development.md
new file mode 100644
index 000000000..4b724e3e9
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/pt/development.md
@@ -0,0 +1,262 @@
+# Estendendo o Bake
+
+O Bake fornece uma arquitetura expansível que permite à sua aplicação ou plugin modificar ou adicionar funcionalidades às funções básicas.
+Bake faz uso de uma classe view dedicada que usa o mecanismo de templates [Twig](https://twig.symfony.com/).
+
+## Eventos do Bake
+
+Como uma classe view, `BakeView` emite os mesmos eventos que qualquer outra classe view, mais um evento extra de inicialização.
+Enquanto as classes view padrão usam o prefixo `View.`, `BakeView` usa o prefixo `Bake.`.
+
+O evento de inicialização pode ser usado para fazer mudanças que se aplicam a todas as saídas do Bake.
+Por exemplo, ao adicionar outro helper à classe view do Bake:
+
+```php
+on('Bake.initialize', function (Event $event) {
+ $view = $event->getSubject();
+
+ // In my bake templates, allow the use of the MySpecial helper
+ $view->loadHelper('MySpecial', ['some' => 'config']);
+
+ // And add an $author variable so it's always available
+ $view->set('author', 'Andy');
+});
+```
+
+Se você deseja modificar o Bake a partir de outro plugin, é recomendável colocar os eventos do plugin no arquivo `config/bootstrap.php`.
+
+Os eventos do Bake podem ser úteis para pequenas alterações nos templates existentes.
+Por exemplo, para alterar os nomes das variáveis usados no controller e template quando executar o Bake:
+
+```php
+on('Bake.beforeRender', function (Event $event) {
+ $view = $event->getSubject();
+
+ // Use $rows for the main data variable in indexes
+ if ($view->get('pluralName')) {
+ $view->set('pluralName', 'rows');
+ }
+ if ($view->get('pluralVar')) {
+ $view->set('pluralVar', 'rows');
+ }
+
+ // Use $theOne for the main data variable in view/edit
+ if ($view->get('singularName')) {
+ $view->set('singularName', 'theOne');
+ }
+ if ($view->get('singularVar')) {
+ $view->set('singularVar', 'theOne');
+ }
+});
+```
+
+Você também pode aplicar os eventos `Bake.beforeRender` e `Bake.afterRender` a um arquivo específico.
+Por exemplo, se quiser adicionar ações ao `UsersController` ao gerar a partir de `Controller/controller.twig`:
+
+```php
+on(
+ 'Bake.beforeRender.Controller.controller',
+ function (Event $event) {
+ $view = $event->getSubject();
+ if ($view->viewVars['name'] == 'Users') {
+ // add the login and logout actions to the Users controller
+ $view->set('actions', [
+ 'login',
+ 'logout',
+ 'index',
+ 'view',
+ 'add',
+ 'edit',
+ 'delete',
+ ]);
+ }
+ }
+);
+```
+
+Ao adicionar listeners específicos para determinados templates do Bake, você simplifica a lógica relacionada ao Bake e fornece callbacks fáceis de testar.
+
+## Sintaxe de Templates do Bake
+
+Os arquivos de template do Bake usam a sintaxe [Twig](https://twig.symfony.com/doc/2.x/).
+
+Então, por exemplo, quando você executar algo como:
+
+```bash
+bin/cake bake shell Foo
+```
+
+O template usado em `vendor/cakephp/bake/src/Template/Bake/Shell/shell.twig` parece com isto:
+
+```php
+` fecha uma tag PHP de template Bake.
+- `<%=` é a forma short-echo do template Bake.
+- `<%-` abre a tag removendo espaços em branco antes dela.
+- `-%>` fecha a tag removendo espaços em branco após ela.
+:::
+
+## Criando um Tema Bake
+
+Se você deseja modificar a saída produzida com o comando `bake`, pode criar seu próprio tema para substituir alguns ou todos os templates que o Bake usa.
+
+1. Gere um novo plugin. O nome do plugin é o nome do tema.
+2. Crie uma nova pasta em `plugins/[name]/Template/Bake/Template/`.
+3. Copie qualquer template que queira modificar de `vendor/cakephp/bake/src/Template/Bake/Template` para a pasta acima e altere conforme sua necessidade.
+4. Ao executar o Bake, use a opção `--theme` para especificar o tema. Para evitar repetir isso a cada chamada, também é possível definir o tema padrão:
+
+```php
+Test->classSuffixes[$this->name()])) {
+ $this->Test->classSuffixes[$this->name()] = 'Foo';
+ }
+
+ $name = ucfirst($this->name());
+ if (!isset($this->Test->classTypes[$name])) {
+ $this->Test->classTypes[$name] = 'Foo';
+ }
+
+ return parent::bakeTest($className);
+}
+```
+
+- O **sufixo da classe** será anexado ao nome fornecido na chamada ao Bake. No exemplo acima, isso criaria `ExampleFooTest.php`.
+- O **tipo de classe** será o subnamespace usado para levar ao arquivo relativo à aplicação ou plugin. No exemplo acima, isso criaria o namespace `App\Test\TestCase\Foo`.
diff --git a/app/vendor/cakephp/bake/docs/pt/development.rst b/app/vendor/cakephp/bake/docs/pt/development.rst
deleted file mode 100644
index 9a24232be..000000000
--- a/app/vendor/cakephp/bake/docs/pt/development.rst
+++ /dev/null
@@ -1,298 +0,0 @@
-Estendendo o Bake
-#################
-
-O Bake fornece uma arquitetura expansível que permite a sua aplicação ou plugin
-modificar ou adicionar funcionalidades às suas funções básicas. Bake faz uso de
-uma classe view dedicada que usa a ferramenta de templates `Twig
- `_.
-
-Eventos do Bake
-===============
-
-Como uma class view , ``BakeView`` emite o mesmo evento como qualquer outra
-classe view, mais uma extra que inicializa eventos. No entanto, onde as classes
-view padrão usam o prefixo "View.", ``BakeView`` usa o prefixo "Bake.".
-
-O inicializador de eventos pode ser usado para fazer mudanças quando aplicado
-a todas as saídas do Bake, por exemplo, ao adicionar outro helper à classe bake
-view este evento pode ser usado::
-
- on('Bake.initialize', function (Event $event) {
- $view = $event->getSubject();
-
- // In my bake templates, allow the use of the MySpecial helper
- $view->loadHelper('MySpecial', ['some' => 'config']);
-
- // And add an $author variable so it's always available
- $view->set('author', 'Andy');
-
- });
-
-Se você deseja modificar o bake de outro plugin, é recomendável colocar os
-eventos do bake do seu plugin no arquivo **config/bootstrap.php**.
-
-Os eventos do Bake podem ser úteis para fazer pequenas alterações nos modelos
-existentes. Por exemplo, para alterar os nomes das variáveis usados no
-controller/template quando executar o bake, pode-se usar uma função esperando
-o ``Bake.beforeRender`` para modificar as variáveis usadas no bake templates::
-
- on('Bake.beforeRender', function (Event $event) {
- $view = $event->getSubject();
-
- // Use $rows for the main data variable in indexes
- if ($view->get('pluralName')) {
- $view->set('pluralName', 'rows');
- }
- if ($view->get('pluralVar')) {
- $view->set('pluralVar', 'rows');
- }
-
- // Use $theOne for the main data variable in view/edit
- if ($view->get('singularName')) {
- $view->set('singularName', 'theOne');
- }
- if ($view->get('singularVar')) {
- $view->set('singularVar', 'theOne');
- }
-
- });
-
-Você também pode abranger os eventos ``Bake.beforeRender``
-e ``Bake.afterRender`` para um arquivo específico. Por exemplo, se você quiser
-adicionar ações específicas para seu UsersController ao gerar a partir de um
-arquivo **Controller/controller.twig**, você pode usar o seguinte evento::
-
- on(
- 'Bake.beforeRender.Controller.controller',
- function (Event $event) {
- $view = $event->getSubject();
- if ($view->viewVars['name'] == 'Users') {
- // add the login and logout actions to the Users controller
- $view->set('actions', [
- 'login',
- 'logout',
- 'index',
- 'view',
- 'add',
- 'edit',
- 'delete'
- ]);
- }
- }
- );
-
-Ao adicionar eventos que escutam um bake templates específico, você pode
-simplesmente relacionar a sua lógica de eventos com o bake e fornecer callbacks
-que são facilmente testáveis.
-
-Sintaxe de Templates do Bake
-============================
-
-Os arquivos de templates do Bake usam a sintaxe `Twig `__.
-
-Então, por exemplo, quando você executar algo como::
-
-.. code-block:: bash
-
- $ bin/cake bake shell Foo
-
-O template usado (**vendor/cakephp/bake/src/Template/Bake/Shell/shell.twig**)
-parece com algo assim::
-
- `` Um template bake php fecha a tag
- * ``<%=`` Um template bake php short-echo tag
- * ``<%-`` Um template bake php abre a tag, retirando qualquer espaço em branco antes da tag
- * ``-%>`` Um template bake php fecha a tag, retirando qualqualquer espaço em branco após a tag
-
-.. _creating-a-bake-theme:
-
-Criando um Tema Bake
-=====================
-
-Se você deseja modificar a saída produzida com o comando bake, você pode criar
-o seu próprio tema para o bake que permitirá você substituir algum ou todos os
-tempaltes que o bake usa. O mmelhor jeito de fazer isto é:
-
-#. Bake um novo plugin. O nome do plugin é o 'nome do tema'
-#. Crie uma nova pasta em **plugins/[name]/Template/Bake/Template/**.
-#. Copie qualquer template que você queira modificar de
- **vendor/cakephp/bake/src/Template/Bake/Template** para a pasta acima e modificá-los conforme sua necessidade.
-#. Quando executar o bake use a opção ``--theme`` para especificar qual o tema
- que o bake deve usar. Para evitar problemas com esta opção, em cada chamada,
- você também pode definir o seu template customizado para ser usado como
- o template padrão::
-
- Test->classSuffixes[$this->name()])) {
- $this->Test->classSuffixes[$this->name()] = 'Foo';
- }
-
- $name = ucfirst($this->name());
- if (!isset($this->Test->classTypes[$name])) {
- $this->Test->classTypes[$name] = 'Foo';
- }
-
- return parent::bakeTest($className);
- }
-
-* O **sufixo da classe** será anexado ao nome fornecido em sua chamada bake. No
- exemplo anterior, ele criaria um arquivo ExampleFooTest.php.
-* O **tipo de classe** será o subdomínio usado que levará ao seu arquivo
- (relativo ao aplicativo ou ao plugin em que você está inserindo). No exemplo
- anterior, ele criaria seu teste com o namespace App\Test\TestCase\Foo.
-
-
-.. meta::
- :title lang=en: Extending Bake
- :keywords lang=en: command line interface,development,bake view, bake template syntax,twig,erb tags,percent tags
-
diff --git a/app/vendor/cakephp/bake/docs/pt/index.md b/app/vendor/cakephp/bake/docs/pt/index.md
new file mode 100644
index 000000000..fb09a83d4
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/pt/index.md
@@ -0,0 +1,25 @@
+# Console Bake
+
+O console do Bake é uma ferramenta para você sair produzindo em CakePHP rapidamente.
+Ele pode criar os itens básicos do CakePHP, como models, behaviors, views, helpers,
+controllers, components, test cases, fixtures e plugins.
+Bake pode ir além de classes esqueleto e gerar uma base funcional em poucos minutos.
+
+## Instalação
+
+Antes de usar ou estender o Bake, tenha certeza de que ele está instalado em sua aplicação.
+Bake é distribuído como um plugin que você pode instalar com Composer:
+
+```bash
+composer require --dev cakephp/bake:~2.0
+```
+
+Isso instala o Bake como dependência de desenvolvimento, portanto ele não será instalado em produção.
+
+Ao usar templates Twig, verifique se você está carregando o plugin `Cake/TwigView` com seu bootstrap.
+Você também pode omiti-lo completamente, o que faz com que o plugin Bake carregue esse plugin sob demanda.
+
+## Mapa da documentação
+
+- [Geração de código com Bake](/pt/usage) cobre a execução do CLI, os comandos disponíveis e os temas do Bake.
+- [Estendendo o Bake](/pt/development) cobre eventos, templates, temas e novos comandos de Bake.
diff --git a/app/vendor/cakephp/bake/docs/pt/index.rst b/app/vendor/cakephp/bake/docs/pt/index.rst
deleted file mode 100644
index 8d068757e..000000000
--- a/app/vendor/cakephp/bake/docs/pt/index.rst
+++ /dev/null
@@ -1,30 +0,0 @@
-Console Bake
-############
-
-O console do **Bake** é outra ferramenta disponível para você sair trabalhando
-- e rápido! O console Bake pode criar qualquer ítem básico do CakePHP: models,
-behaviors, views, helpers, controllers, components, test cases, fixtures
-e plugins. E nós não estamos apenas falando do esqueleto da classes: O Bake
-pode criar uma aplicação totalmente funcional em questão de minutos. De fato,
-o Bake é um passo natural a se dar uma vez que a aplicação tem sua base
-construída.
-
-Instalação
-==========
-
-Antes de tentar usar ou estender o Bake, tenha certeza de que ele está instalado em
-sua aplicação. O Bake é distribuído como um plugin que você pode instalar com o
-Composer::
-
- composer require --dev cakephp/bake:~2.0
-
-Isto irá instalar o Bake como uma dependência de desenvolvimento, sendo assim,
-não será instalado no ambiente de produção.
-
-Ao usar os modelos Twig, verifique se você está carregando o plugin
-Cake/TwigView com seu bootstrap. Você também pode omiti-lo completamente,
-o que faz com que o plugin Bake carregue esse plugin sob demanda.
-
-.. meta::
- :title lang=pt: Bake Console
- :keywords lang=pt: cli,linha de comando,command line,dev,desenvolvimento,bake view, bake syntax,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/pt/usage.md b/app/vendor/cakephp/bake/docs/pt/usage.md
new file mode 100644
index 000000000..fd89ed1ae
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/pt/usage.md
@@ -0,0 +1,104 @@
+# Geração de Código com Bake
+
+O console do Bake é executado usando o PHP CLI.
+Se você tiver problemas para executar o script, assegure-se de que:
+
+1. Você instalou o PHP CLI e possui os módulos apropriados habilitados, por exemplo MySQL e `intl`.
+2. Se o host do banco de dados for `localhost`, tente `127.0.0.1`, pois `localhost` pode causar problemas no PHP CLI.
+3. Dependendo de como o seu computador está configurado, pode ser necessário definir permissões de execução no script Cake para chamá-lo com `bin/cake bake`.
+
+Antes de executar o Bake, você deve ter pelo menos uma conexão de banco de dados configurada.
+
+Para ver as opções disponíveis no Bake, digite:
+
+```bash
+$ bin/cake bake --help
+
+Current Paths:
+
+* app: src
+* root: .
+* core: .\vendor\cakephp\cakephp
+
+Available Commands:
+
+Bake:
+ - bake all
+ - bake behavior
+ - bake cell
+ - bake command
+ - bake component
+ - bake controller
+ - bake controller all
+ - bake fixture
+ - bake fixture all
+ - bake form
+ - bake helper
+ - bake mailer
+ - bake middleware
+ - bake model
+ - bake model all
+ - bake plugin
+ - bake task
+ - bake template
+ - bake template all
+ - bake test
+
+To run a command, type `cake command_name [args|options]`
+To get help on a specific command, type `cake command_name --help`
+```
+
+Você pode obter mais informações sobre o que cada tarefa faz e quais são suas opções usando `--help`:
+
+```bash
+$ bin/cake bake model --help
+
+Bake table and entity classes.
+
+Usage:
+cake bake model [options] []
+
+Options:
+
+--connection, -c The datasource connection to get data from.
+ (default: default)
+--display-field The displayField if you would like to choose one.
+--fields A comma separated list of fields to make
+ accessible.
+--force, -f Force overwriting existing files without
+ prompting.
+--help, -h Display this help.
+--hidden A comma separated list of fields to hide.
+--no-associations Disable generating associations.
+--no-entity Disable generating an entity class.
+--no-fields Disable generating accessible fields in the
+ entity.
+--no-fixture Do not generate a test fixture skeleton.
+--no-hidden Disable generating hidden fields in the entity.
+--no-rules Disable generating a rules checker.
+--no-table Disable generating a table class.
+--no-test Do not generate a test case skeleton.
+--no-validation Disable generating validation rules.
+--plugin, -p Plugin to bake into.
+--primary-key The primary key if you would like to manually set
+ one. Can be a comma separated list if you are
+ using a composite primary key.
+--quiet, -q Enable quiet output.
+--table The table name to use if you have
+ non-conventional table names.
+--theme, -t The theme to use when baking code.
+--verbose, -v Enable verbose output.
+
+Arguments:
+
+name Name of the model to bake (without the Table suffix). You can use
+ Plugin.name to bake plugin models. (optional)
+
+Omitting all arguments and options will list the table names you can
+generate models for.
+```
+
+## Temas para o Bake
+
+A opção `theme` é comum a todos os comandos do Bake e permite mudar os arquivos de template usados por ele.
+Para criar seus próprios templates, veja [Criando um Tema Bake](/pt/development#criando-um-tema-bake).
diff --git a/app/vendor/cakephp/bake/docs/pt/usage.rst b/app/vendor/cakephp/bake/docs/pt/usage.rst
deleted file mode 100644
index 7d513a0f6..000000000
--- a/app/vendor/cakephp/bake/docs/pt/usage.rst
+++ /dev/null
@@ -1,117 +0,0 @@
-Geração de Código com Bake
-##########################
-
-O console do **Bake** é executado usando o PHP CLI (interface da linha de comando).
-Se você tiver problemas para executar o script, assegure-se de que:
-
-#. Você instalou o PHP CLI e possui os módulos apropriados habilitados (por
- exemplo: MySQL, intl).
-#. Os usuários também podem ter problemas se o host do banco de dados for
- 'localhost' e devem tentar '127.0.0.1', em vez disso, como localhost pode
- causar problemas no PHP CLI.
-#. Dependendo de como o seu computador está configurado, você pode ter que
- definir direitos de execução no script cake bash para chamá-lo usando
- ``bin/cake bake``.
-
-Antes de executar o Bake você deve certificar-se de ter pelo menos um banco de dados com a conexão configurada.
-
-Para ver as opções disponíveis no Bake digite::
-
- $ bin/cake bake --help
-
- Current Paths:
-
- * app: src
- * root: .
- * core: .\vendor\cakephp\cakephp
-
- Available Commands:
-
- Bake:
- - bake all
- - bake behavior
- - bake cell
- - bake command
- - bake component
- - bake controller
- - bake controller all
- - bake fixture
- - bake fixture all
- - bake form
- - bake helper
- - bake mailer
- - bake middleware
- - bake model
- - bake model all
- - bake plugin
- - bake shell
- - bake shell_helper
- - bake task
- - bake template
- - bake template all
- - bake test
-
- To run a command, type `cake command_name [args|options]`
- To get help on a specific command, type `cake command_name --help`
-
-
-Você pode obter mais informações sobre o que cada tarefa faz e quais são suas opções
-disponíveis usando a opção ``--help``::
-
- $ bin/cake bake model --help
-
- Bake table and entity classes.
-
- Usage:
- cake bake model [options] []
-
- Options:
-
- --connection, -c The datasource connection to get data from.
- (default: default)
- --display-field The displayField if you would like to choose one.
- --fields A comma separated list of fields to make
- accessible.
- --force, -f Force overwriting existing files without
- prompting.
- --help, -h Display this help.
- --hidden A comma separated list of fields to hide.
- --no-associations Disable generating associations.
- --no-entity Disable generating an entity class.
- --no-fields Disable generating accessible fields in the
- entity.
- --no-fixture Do not generate a test fixture skeleton.
- --no-hidden Disable generating hidden fields in the entity.
- --no-rules Disable generating a rules checker.
- --no-table Disable generating a table class.
- --no-test Do not generate a test case skeleton.
- --no-validation Disable generating validation rules.
- --plugin, -p Plugin to bake into.
- --primary-key The primary key if you would like to manually set
- one. Can be a comma separated list if you are
- using a composite primary key.
- --quiet, -q Enable quiet output.
- --table The table name to use if you have
- non-conventional table names.
- --theme, -t The theme to use when baking code.
- --verbose, -v Enable verbose output.
-
- Arguments:
-
- name Name of the model to bake (without the Table suffix). You can use
- Plugin.name to bake plugin models. (optional)
-
- Omitting all arguments and options will list the table names you can
- generate models for.
-
-
-
-Temas para o Bake
-=================
-
-A opção de tema é comum a todos os comandos do Bake e permite mudar os arquivos de modelo usados por ele. Para criar seus próprios modelos, veja a
-:ref:`documentação de criação de temas para o Bake `.
-
-.. meta::
- :title lang=pt: Geração de código com bake
- :keywords lang=pt: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/apple-touch-icon.png b/app/vendor/cakephp/bake/docs/public/favicon/apple-touch-icon.png
new file mode 100644
index 000000000..c6d073d7b
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/favicon/apple-touch-icon.png differ
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/favicon-96x96.png b/app/vendor/cakephp/bake/docs/public/favicon/favicon-96x96.png
new file mode 100644
index 000000000..6642e0cda
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/favicon/favicon-96x96.png differ
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/favicon.ico b/app/vendor/cakephp/bake/docs/public/favicon/favicon.ico
new file mode 100644
index 000000000..405aa94ce
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/favicon/favicon.ico differ
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/favicon.svg b/app/vendor/cakephp/bake/docs/public/favicon/favicon.svg
new file mode 100644
index 000000000..805ef4b8f
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/public/favicon/favicon.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/site.webmanifest b/app/vendor/cakephp/bake/docs/public/favicon/site.webmanifest
new file mode 100644
index 000000000..4f23fb31d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/public/favicon/site.webmanifest
@@ -0,0 +1,21 @@
+{
+ "name": "CakePHP",
+ "short_name": "CakePHP",
+ "icons": [
+ {
+ "src": "/favicon/web-app-manifest-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/favicon/web-app-manifest-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
\ No newline at end of file
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/web-app-manifest-192x192.png b/app/vendor/cakephp/bake/docs/public/favicon/web-app-manifest-192x192.png
new file mode 100644
index 000000000..b5df2990b
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/favicon/web-app-manifest-192x192.png differ
diff --git a/app/vendor/cakephp/bake/docs/public/favicon/web-app-manifest-512x512.png b/app/vendor/cakephp/bake/docs/public/favicon/web-app-manifest-512x512.png
new file mode 100644
index 000000000..6a522de35
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/favicon/web-app-manifest-512x512.png differ
diff --git a/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.eot b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.eot
new file mode 100644
index 000000000..0800d1e7d
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.eot differ
diff --git a/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.svg b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.svg
new file mode 100644
index 000000000..d2afda5e2
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.svg
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.ttf b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.ttf
new file mode 100644
index 000000000..78ad6c884
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.ttf differ
diff --git a/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.woff b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.woff
new file mode 100644
index 000000000..a95e1b38b
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.woff differ
diff --git a/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.woff2 b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.woff2
new file mode 100644
index 000000000..2cd9fdd0e
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/fonts/cakedingbats-webfont.woff2 differ
diff --git a/app/vendor/cakephp/bake/docs/public/history-panel-use.mp4 b/app/vendor/cakephp/bake/docs/public/history-panel-use.mp4
new file mode 100644
index 000000000..d87dc65ab
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/history-panel-use.mp4 differ
diff --git a/app/vendor/cakephp/bake/docs/public/history-panel.png b/app/vendor/cakephp/bake/docs/public/history-panel.png
new file mode 100644
index 000000000..0c6b27e20
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/history-panel.png differ
diff --git a/app/vendor/cakephp/bake/docs/public/logo.svg b/app/vendor/cakephp/bake/docs/public/logo.svg
new file mode 100644
index 000000000..829c8e98d
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/public/logo.svg
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/app/vendor/cakephp/bake/docs/public/mail-panel.mp4 b/app/vendor/cakephp/bake/docs/public/mail-panel.mp4
new file mode 100644
index 000000000..02078c858
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/mail-panel.mp4 differ
diff --git a/app/vendor/cakephp/bake/docs/public/mail-previewer.mp4 b/app/vendor/cakephp/bake/docs/public/mail-previewer.mp4
new file mode 100644
index 000000000..66f9fdb97
Binary files /dev/null and b/app/vendor/cakephp/bake/docs/public/mail-previewer.mp4 differ
diff --git a/app/vendor/cakephp/bake/docs/ru/conf.py b/app/vendor/cakephp/bake/docs/ru/conf.py
deleted file mode 100644
index f8a170ee5..000000000
--- a/app/vendor/cakephp/bake/docs/ru/conf.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import sys, os
-
-# Append the top level directory of the docs, so we can import from the config dir.
-sys.path.insert(0, os.path.abspath('..'))
-
-# Pull in all the configuration options defined in the global config file..
-from config.all import *
-
-language = 'ru'
diff --git a/app/vendor/cakephp/bake/docs/ru/contents.rst b/app/vendor/cakephp/bake/docs/ru/contents.rst
deleted file mode 100644
index 08c3e957c..000000000
--- a/app/vendor/cakephp/bake/docs/ru/contents.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-.. toctree::
- :maxdepth: 2
- :caption: CakePHP Bake
-
- /index
- /usage
- /development
diff --git a/app/vendor/cakephp/bake/docs/ru/development.md b/app/vendor/cakephp/bake/docs/ru/development.md
new file mode 100644
index 000000000..47dda1b25
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/ru/development.md
@@ -0,0 +1,263 @@
+# Расширение возможностей Bake
+
+Bake имеет расширяемую архитектуру, которая позволяет вашему приложению или плагинам изменять или дополнять базовую функциональность.
+Bake использует специальный класс представления и механизм шаблонизатора [Twig](https://twig.symfony.com/).
+
+## События Bake
+
+`BakeView`, как и любой другой класс представления, генерирует стандартные события, а также дополнительное событие инициализации.
+Стандартные классы представления используют префикс `View.`, а `BakeView` использует префикс `Bake.`.
+
+Событие `initialize` можно использовать для внесения изменений, которые применяются ко всему выводу Bake.
+Например, чтобы добавить helper в класс представления Bake:
+
+```php
+on('Bake.initialize', function (Event $event) {
+ $view = $event->getSubject();
+
+ // В моих шаблонах bake разрешить использование MySpecial helper
+ $view->loadHelper('MySpecial', ['some' => 'config']);
+
+ // И добавить переменную $author, чтобы она всегда была доступна
+ $view->set('author', 'Andy');
+});
+```
+
+Если вы хотите изменить Bake из другого плагина, удобнее всего разместить события плагина в `config/bootstrap.php`.
+
+События Bake полезны и для небольших изменений существующих шаблонов.
+Например, чтобы изменить имена переменных, используемых при генерации controller и template файлов, можно слушать `Bake.beforeRender`:
+
+```php
+on('Bake.beforeRender', function (Event $event) {
+ $view = $event->getSubject();
+
+ // Использовать $rows для основной переменной данных в index
+ if ($view->get('pluralName')) {
+ $view->set('pluralName', 'rows');
+ }
+ if ($view->get('pluralVar')) {
+ $view->set('pluralVar', 'rows');
+ }
+
+ // Использовать $theOne для основной переменной данных в view/edit
+ if ($view->get('singularName')) {
+ $view->set('singularName', 'theOne');
+ }
+ if ($view->get('singularVar')) {
+ $view->set('singularVar', 'theOne');
+ }
+});
+```
+
+Вы также можете привязать события `Bake.beforeRender` и `Bake.afterRender` к конкретному генерируемому файлу.
+Например, если вы хотите добавить действия в `UsersController` при генерации из `Controller/controller.twig`:
+
+```php
+on(
+ 'Bake.beforeRender.Controller.controller',
+ function (Event $event) {
+ $view = $event->getSubject();
+ if ($view->viewVars['name'] == 'Users') {
+ // добавим действия входа и выхода в контроллер Users
+ $view->viewVars['actions'] = [
+ 'login',
+ 'logout',
+ 'index',
+ 'view',
+ 'add',
+ 'edit',
+ 'delete',
+ ];
+ }
+ }
+);
+```
+
+Фокусируя обработчики на конкретных шаблонах Bake, вы упрощаете связанную с Bake логику событий и получаете более удобные для тестирования callback-функции.
+
+## Синтаксис шаблонов Bake
+
+Файлы шаблонов Bake используют синтаксис [Twig](https://twig.symfony.com/doc/2.x/).
+
+Например, при генерации shell-команды:
+
+```bash
+bin/cake bake shell Foo
+```
+
+Шаблон `vendor/cakephp/bake/src/Template/Bake/Shell/shell.twig` выглядит так:
+
+```php
+` закрывающий PHP-тег шаблона Bake.
+- `<%=` короткий echo-тег шаблона Bake.
+- `<%-` открывающий тег с удалением пробелов перед тегом.
+- `-%>` закрывающий тег с удалением пробелов после тега.
+:::
+
+## Создание темы Bake
+
+Если вы хотите изменить вывод, создаваемый командой `bake`, вы можете создать собственную тему Bake, которая позволит заменить часть или все шаблоны.
+
+1. Сгенерируйте новый плагин. Имя плагина станет именем темы Bake.
+2. Создайте директорию `plugins/[name]/src/Template/Bake/Template/`.
+3. Скопируйте нужные шаблоны из `vendor/cakephp/bake/src/Template/Bake/Template` в соответствующие файлы вашего плагина.
+4. При запуске Bake используйте параметр `--theme`, чтобы указать тему. Чтобы не передавать его каждый раз, можно настроить тему по умолчанию:
+
+```php
+Test->classSuffixes[$this->name()])) {
+ $this->Test->classSuffixes[$this->name()] = 'Foo';
+ }
+
+ $name = ucfirst($this->name());
+ if (!isset($this->Test->classTypes[$name])) {
+ $this->Test->classTypes[$name] = 'Foo';
+ }
+
+ return parent::bakeTest($className);
+}
+```
+
+- **Суффикс класса** добавляется к имени, переданному в вызове `bake`. В примере выше это создаст `ExampleFooTest.php`.
+- **Тип класса** определяет подпространство имён, ведущее к файлу относительно приложения или плагина. В примере выше это создаст тест с namespace `App\Test\TestCase\Foo`.
diff --git a/app/vendor/cakephp/bake/docs/ru/development.rst b/app/vendor/cakephp/bake/docs/ru/development.rst
deleted file mode 100644
index 568068dbf..000000000
--- a/app/vendor/cakephp/bake/docs/ru/development.rst
+++ /dev/null
@@ -1,288 +0,0 @@
-Расширение возможностей Bake
-############################
-
-Bake имеет расширяемую архитектуру, которая позволяет вашему приложению или плагинам
-измененять или добавления его базовую функциональность. Bake использует специальный
-класс, который использует механизм шаблонизатора `Twig `_.
-
-События Bake
-============
-
-В качестве класса представления ``BakeView`` генерирует те же события, что и любой другой класс представления,
-плюс одно дополнительное событие инициализации. Однако, в то время как классы стандартного представления используют
-префикс события "View.", ``BakeView`` использует префикс события "Bake.".
-
-Событие initialize можно использовать для внесения изменений, которые применяются ко всем 'испеченным'
-выводам, например, чтобы добавить другого помощника в класс вида bake, это событие может
-использовать следующее::
-
- on('Bake.initialize', function (Event $event) {
- $view = $event->getSubject();
-
- // В моих шаблонах bake, разрешить использование MySpecial helper
- $view->loadHelper('MySpecial', ['some' => 'config']);
-
- // И добавьте переменную $author, чтобы она всегда была доступна
- $view->set('author', 'Andy');
-
- });
-
-Если вы хотите изменить выпечку из другого плагина, хорошей идеей будет - поместить
-события своего плагина в файл ``config/bootstrap.php``.
-
-События Bake могут быть полезны для внесения небольших изменений в существующие шаблоны.
-Например, чтобы изменить имена переменных, используемых для выпечки файлов контроллеров/шаблонов,
-можно использовать прослушивающую функцию ``Bake.beforeRender``::
-
- on('Bake.beforeRender', function (Event $event) {
- $view = $event->getSubject();
-
- // Использовать $rows для основной переменной данных для индекса (index)
- if ($view->get('pluralName')) {
- $view->set('pluralName', 'rows');
- }
- if ($view->get('pluralVar')) {
- $view->set('pluralVar', 'rows');
- }
-
- // Используйте $theOne для основной переменной данных для просмотра/редактирования (view/edit)
- if ($view->get('singularName')) {
- $view->set('singularName', 'theOne');
- }
- if ($view->get('singularVar')) {
- $view->set('singularVar', 'theOne');
- }
-
- });
-
-Вы также можете использовать события ``Bake.beforeRender`` и ``Bake.afterRender`` для
-генерирования специфичного файла. Например, если вы хотите добавить определенные действия для
-вашего UserController при генерации из файла **Controller/controller.twig**,
-вы можете использовать следующее событие::
-
- on(
- 'Bake.beforeRender.Controller.controller',
- function (Event $event) {
- $view = $event->getSubject();
- if ($view->viewVars['name'] == 'Users') {
- // добавим действия входа и выхода из системы в контроллер пользователей
- $view->viewVars['actions'] = [
- 'login',
- 'logout',
- 'index',
- 'view',
- 'add',
- 'edit',
- 'delete'
- ];
- }
- }
- );
-
-При просмотре прослушивателей событий для конкретных шаблонов выпечки вы можете упростить
-выпечку связанной логики событий и обеспечить обратные вызовы, которые легче тестировать.
-
-Синтаксис шаблона выпечки
-=========================
-
-Файлы шаблонов Bake используют синтаксис шаблонизатора `Twig `__.
-
-Так, например, при выпечке такой оболочки:
-
-.. code-block:: bash
-
- bin/cake bake shell Foo
-
-Используемый шаблон (**vendor/cakephp/bake/src/Template/Bake/Shell/shell.twig**)
-будет выглядеть так::
-
- `` Bake шаблон закрывающего тега php
- * ``<%=`` Bake шаблон php короткого php тега echo
- * ``<%-`` Bake шаблон php открытия тега, удаляющего любые введённые пробелы перед тегом
- * ``-%>`` Bake шаблон php закрытия тега, любые введённые пробелы после тега
-
-.. _creating-a-bake-theme:
-
-Создание темы Bake
-==================
-
-Если вы хотите изменить результат, полученный командой "bake", вы можете
-создать свою собственную тему('theme') для "bake", которая позволит вам заменить некоторые или все
-шаблоны, которые испекает bake. Лучше всего сделать это так:
-
-#. Выпечь новый плагин. Имя плагина - это название темы 'bake'
-#. Создать новую диреткорию **plugins/[name]/src/Template/Bake/Template/**.
-#. Скопируйте любые шаблоны, которые вы хотите переопределить
- **vendor/cakephp/bake/src/Template/Bake/Template** для сопоставления файлов с вашим плагином.
-#. При запуске выпечки используйте параметр ``--theme``, чтобы указать тему выпечки которую вы
- хотите использовать. Чтобы избежать необходимости указывать этот параметр в каждом вызове, вы также можете
- настроить свою тему в качестве темы по умолчанию::
-
- Test->classSuffixes[$this->name()])) {
- $this->Test->classSuffixes[$this->name()] = 'Foo';
- }
-
- $name = ucfirst($this->name());
- if (!isset($this->Test->classTypes[$name])) {
- $this->Test->classTypes[$name] = 'Foo';
- }
-
- return parent::bakeTest($className);
- }
-
-* **Суффикс класса** будет добавлен к имени, указанному в вашем вызове ``bake``.
- В предыдущем примере он создал бы файл ``ExampleFooTest.php``.
-
-* **Тип файла** будет использовать пространство под-пространство имён(sub-namespace), которое приведёт к файлу (относительно приложения или подключаемого модуля). В предыдущем примере, он создаст ваш тест с пространством имен ``App\Test\TestCase\Foo``.
-
-.. meta::
- :title lang=ru: Расширение возможностей Bake
- :keywords lang=ru: интерфейс командной строки, разработка, выпечка, синтаксис шаблона выпечки, твинг, метки erb, процентные теги
diff --git a/app/vendor/cakephp/bake/docs/ru/index.md b/app/vendor/cakephp/bake/docs/ru/index.md
new file mode 100644
index 000000000..d330e7319
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/ru/index.md
@@ -0,0 +1,23 @@
+# Консоль Bake
+
+Консоль Bake для CakePHP помогает быстро начать разработку.
+Она может создать базовые компоненты CakePHP, включая models, behaviors, views,
+helpers, controllers, components, test cases, fixtures и plugins.
+Bake умеет создавать не только каркас приложения, но и полностью рабочую основу за считанные минуты.
+
+## Установка
+
+Перед тем как использовать Bake, убедитесь, что он установлен в вашем приложении.
+Bake распространяется как отдельный плагин, который можно установить через Composer:
+
+```bash
+composer require --dev cakephp/bake:~1.0
+```
+
+Команда выше установит Bake как зависимость для разработки.
+Это значит, что Bake не будет установлен при деплое на production.
+
+## Карта документации
+
+- [Генерация кода с помощью Bake](/ru/usage) описывает запуск CLI, список задач и темы Bake.
+- [Расширение возможностей Bake](/ru/development) описывает события, шаблоны, темы и добавление собственных команд.
diff --git a/app/vendor/cakephp/bake/docs/ru/index.rst b/app/vendor/cakephp/bake/docs/ru/index.rst
deleted file mode 100644
index 0b9c1bc80..000000000
--- a/app/vendor/cakephp/bake/docs/ru/index.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-Консоль Bake
-############
-
-Bake консоль CakePHP - это еще один инструмент для быстрой разработки на фреймворке CakePHP.
-Консоль Bake может создать любой из базовых компонентов или все сразу: models,
-behaviors, views, helpers, controllers, components, test cases, fixtures и plugins.
-Речь идет не только о каркасе приложения: Bake консоль создает полностью функционирующее приложение всего за пару минут.
-
-Bake это естественный шаг для статической генерации кода.
-
-Установка
-=========
-
-Перед тем, как использовать Bake, убедитесь, что он установлен в вашем приложении.
-Bake это отдельный плагин и вы можете установить его с помощью Composer::
-
- composer require --dev cakephp/bake:~1.0
-
-Команда выше установит Bake в зависимости от разработки.
-Это значит, что Bake не будет установлен, когда вы делаете деплой на продакшн сервер.
-
-.. meta::
- :title lang=ru: Консоль Bake
- :keywords lang=ru: интерфейс командной строки,разработка,bake view, bake template syntax,erb tags,asp tags,percent tags
diff --git a/app/vendor/cakephp/bake/docs/ru/usage.md b/app/vendor/cakephp/bake/docs/ru/usage.md
new file mode 100644
index 000000000..97207aefd
--- /dev/null
+++ b/app/vendor/cakephp/bake/docs/ru/usage.md
@@ -0,0 +1,117 @@
+# Генерация кода с помощью Bake
+
+Консоль Cake запускается с использованием PHP CLI.
+Если у вас возникли проблемы с запуском скрипта, убедитесь, что:
+
+1. У вас установлен PHP CLI и включены нужные модули, например MySQL и `intl`.
+2. Если хост базы данных указан как `localhost`, попробуйте `127.0.0.1`, потому что `localhost` может вызывать проблемы в PHP CLI.
+3. В зависимости от того, как настроен ваш компьютер, вам может потребоваться выдать права на выполнение скрипта, чтобы запускать `bin/cake bake`.
+
+Перед запуском Bake вы должны убедиться, что у вас настроено хотя бы одно соединение с базой данных.
+
+При запуске без аргументов `bin/cake bake` выводит список доступных задач.
+Вы должны увидеть что-то вроде:
+
+```bash
+$ bin/cake bake
+
+Welcome to CakePHP v3.4.6 Console
+---------------------------------------------------------------
+App : src
+Path: /var/www/cakephp.dev/src/
+PHP : 5.6.20
+---------------------------------------------------------------
+The following commands can be used to generate skeleton code for your application.
+
+Available bake commands:
+
+- all
+- behavior
+- cell
+- component
+- controller
+- fixture
+- form
+- helper
+- mailer
+- migration
+- migration_diff
+- migration_snapshot
+- model
+- plugin
+- seed
+- task
+- template
+- test
+
+By using `cake bake [name]` you can invoke a specific bake task.
+```
+
+Дополнительную информацию о задачах и доступных параметрах можно получить через `--help`:
+
+```bash
+$ bin/cake bake --help
+
+Welcome to CakePHP v3.4.6 Console
+---------------------------------------------------------------
+App : src
+Path: /var/www/cakephp.dev/src/
+PHP : 5.6.20
+---------------------------------------------------------------
+The Bake script generates controllers, models and template files for
+your application. If run with no command line arguments, Bake guides the
+user through the class creation process. You can customize the
+generation process by telling Bake where different parts of your
+application are using command line arguments.
+
+Usage:
+cake bake.bake [subcommand] [options]
+
+Subcommands:
+
+all Bake a complete MVC skeleton.
+behavior Bake a behavior class file.
+cell Bake a cell class file.
+component Bake a component class file.
+controller Bake a controller skeleton.
+fixture Generate fixtures for use with the test suite. You
+ can use `bake fixture all` to bake all fixtures.
+form Bake a form class file.
+helper Bake a helper class file.
+mailer Bake a mailer class file.
+migration Bake migration class.
+migration_diff Bake migration class.
+migration_snapshot Bake migration snapshot class.
+model Bake table and entity classes.
+plugin Create the directory structure, AppController class
+ and testing setup for a new plugin. Can create
+ plugins in any of your bootstrapped plugin paths.
+seed Bake seed class.
+task Bake a task class file.
+template Bake views for a controller, using built-in or
+ custom templates.
+test Bake test case skeletons for classes.
+
+To see help on a subcommand use `cake bake.bake [subcommand] --help`
+
+Options:
+
+--connection, -c Database connection to use in conjunction with `bake
+ all`. (default: default)
+--everything Bake a complete MVC skeleton, using all the available
+ tables. Usage: "bake all --everything"
+--force, -f Force overwriting existing files without prompting.
+--help, -h Display this help.
+--plugin, -p Plugin to bake into.
+--prefix Prefix to bake controllers and templates into.
+--quiet, -q Enable quiet output.
+--tablePrefix Table prefix to be used in models.
+--theme, -t The theme to use when baking code. (choices:
+ Bake|Migrations)
+--verbose, -v Enable verbose output.
+```
+
+## Темы Bake
+
+Параметр `theme` является общим для всех команд Bake и позволяет изменять файлы шаблонов, используемые при генерации.
+Чтобы создать свои шаблоны, см. [Создание темы Bake](/ru/development#создание-темы-bake).
diff --git a/app/vendor/cakephp/bake/docs/ru/usage.rst b/app/vendor/cakephp/bake/docs/ru/usage.rst
deleted file mode 100644
index 8cfd72b85..000000000
--- a/app/vendor/cakephp/bake/docs/ru/usage.rst
+++ /dev/null
@@ -1,131 +0,0 @@
-Генерация кода с помощью Bake
-#############################
-
-Консоль cake запускается с использованием PHP CLI (интерфейса командной строки).
-Если у вас возникли проблемы с запуском скрипта, убедитесь, что:
-
-#. У вас установлен PHP CLI и что у него есть соответствующие включённые модули
- (например: MySQL, intl).
-#. У пользователей также могут быть проблемы, если хост базы данных является
- 'localhost' вмеcто этого можно попробовать '127.0.0.1', поскольку localhost
- может вызвать проблемы с PHP CLI.
-#. В зависимости от того, как настроен ваш компьютер, вам может потребоваться
- установить права выполнения на скрипт bash, чтобы вызвать его, используя
- ``bin/cake bake``.
-
-Перед запуском bake вы должны убедиться, что у вас есть хотя бы одна база данных и
-соединение настроено.
-
-При запуске без аргументов ``bin/cake bake`` выводит список доступных
-заданий. Вы должны увидеть что-то вроде::
-
- $ bin/cake bake
-
- Welcome to CakePHP v3.4.6 Console
- ---------------------------------------------------------------
- App : src
- Path: /var/www/cakephp.dev/src/
- PHP : 5.6.20
- ---------------------------------------------------------------
- The following commands can be used to generate skeleton code for your application.
-
- Available bake commands:
-
- - all
- - behavior
- - cell
- - component
- - controller
- - fixture
- - form
- - helper
- - mailer
- - migration
- - migration_diff
- - migration_snapshot
- - model
- - plugin
- - seed
- - shell
- - shell_helper
- - task
- - template
- - test
-
- By using `cake bake [name]` you can invoke a specific bake task.
-
-Вы можете получить больше дополнительной информации о том, что делает каждая задача, и какие параметры
-доступны, если используете ключ ``--help``::
-
- $ bin/cake bake --help
-
- Welcome to CakePHP v3.4.6 Console
- ---------------------------------------------------------------
- App : src
- Path: /var/www/cakephp.dev/src/
- PHP : 5.6.20
- ---------------------------------------------------------------
- The Bake script generates controllers, models and template files for
- your application. If run with no command line arguments, Bake guides the
- user through the class creation process. You can customize the
- generation process by telling Bake where different parts of your
- application are using command line arguments.
-
- Usage:
- cake bake.bake [subcommand] [options]
-
- Subcommands:
-
- all Bake a complete MVC skeleton.
- behavior Bake a behavior class file.
- cell Bake a cell class file.
- component Bake a component class file.
- controller Bake a controller skeleton.
- fixture Generate fixtures for use with the test suite. You
- can use `bake fixture all` to bake all fixtures.
- form Bake a form class file.
- helper Bake a helper class file.
- mailer Bake a mailer class file.
- migration Bake migration class.
- migration_diff Bake migration class.
- migration_snapshot Bake migration snapshot class.
- model Bake table and entity classes.
- plugin Create the directory structure, AppController class
- and testing setup for a new plugin. Can create
- plugins in any of your bootstrapped plugin paths.
- seed Bake seed class.
- shell Bake a shell class file.
- shell_helper Bake a shell_helper class file.
- task Bake a task class file.
- template Bake views for a controller, using built-in or
- custom templates.
- test Bake test case skeletons for classes.
-
- To see help on a subcommand use `cake bake.bake [subcommand] --help`
-
- Options:
-
- --connection, -c Database connection to use in conjunction with `bake
- all`. (default: default)
- --everything Bake a complete MVC skeleton, using all the available
- tables. Usage: "bake all --everything"
- --force, -f Force overwriting existing files without prompting.
- --help, -h Display this help.
- --plugin, -p Plugin to bake into.
- --prefix Prefix to bake controllers and templates into.
- --quiet, -q Enable quiet output.
- --tablePrefix Table prefix to be used in models.
- --theme, -t The theme to use when baking code. (choices:
- Bake|Migrations)
- --verbose, -v Enable verbose output.
-
-Темы Bake
-=========
-
-Параметр темы является общим для всех команд bake, так же bake позволяет изменять
-файлы шаблонов, используемые при 'выпечке'. Чтобы создать свои собственные шаблоны см.
-:ref:`документация по созданию темы`.
-
-.. meta::
- :title lang=ru: Генерация кода с помощью Bake
- :keywords lang=en: command line interface,functional application,database,database configuration,bash script,basic ingredients,project,model,path path,code generation,scaffolding,windows users,configuration file,few minutes,config,iew,shell,models,running,mysql
diff --git a/app/vendor/cakephp/bake/phpcs.xml b/app/vendor/cakephp/bake/phpcs.xml
index f52106709..16a351cd6 100644
--- a/app/vendor/cakephp/bake/phpcs.xml
+++ b/app/vendor/cakephp/bake/phpcs.xml
@@ -1,14 +1,16 @@
-
-
+
src/
tests/
+
+
+
+
*/comparisons/*
- /tests/test_app/tests/
- /tests/test_app/Plugin/TestBake/
+ tests/test_app/*
diff --git a/app/vendor/cakephp/bake/phpstan-baseline.neon b/app/vendor/cakephp/bake/phpstan-baseline.neon
new file mode 100644
index 000000000..e45840cfa
--- /dev/null
+++ b/app/vendor/cakephp/bake/phpstan-baseline.neon
@@ -0,0 +1,19 @@
+parameters:
+ ignoreErrors:
+ -
+ message: '#^Method Bake\\BakePlugin\:\:bootstrap\(\) has parameter \$app with generic interface Cake\\Core\\PluginApplicationInterface but does not specify its types\: TSubject$#'
+ identifier: missingType.generics
+ count: 1
+ path: src/BakePlugin.php
+
+ -
+ message: '#^Instanceof between mixed and Cake\\Chronos\\Chronos will always evaluate to false\.$#'
+ identifier: instanceof.alwaysFalse
+ count: 1
+ path: src/Command/FixtureCommand.php
+
+ -
+ message: '#^Dead catch \- UnexpectedValueException is never thrown in the try block\.$#'
+ identifier: catch.neverThrown
+ count: 1
+ path: src/Command/TestCommand.php
diff --git a/app/vendor/cakephp/bake/phpstan.neon b/app/vendor/cakephp/bake/phpstan.neon
index ac6727fb6..49655075a 100644
--- a/app/vendor/cakephp/bake/phpstan.neon
+++ b/app/vendor/cakephp/bake/phpstan.neon
@@ -1,7 +1,11 @@
+includes:
+ - phpstan-baseline.neon
+
parameters:
- level: 6
- checkMissingIterableValueType: false
- paths:
- - src/
- bootstrapFiles:
- - tests/bootstrap.php
+ level: 8
+ paths:
+ - src/
+ bootstrapFiles:
+ - tests/bootstrap.php
+ ignoreErrors:
+ - identifier: missingType.iterableValue
diff --git a/app/vendor/cakephp/bake/psalm-baseline.xml b/app/vendor/cakephp/bake/psalm-baseline.xml
deleted file mode 100644
index a49882bc1..000000000
--- a/app/vendor/cakephp/bake/psalm-baseline.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
- new Filesystem()
-
-
- findRecursive
-
-
-
-
- new Filesystem()
-
-
- find
-
-
-
diff --git a/app/vendor/cakephp/bake/psalm.xml b/app/vendor/cakephp/bake/psalm.xml
deleted file mode 100644
index 8f0d13b5c..000000000
--- a/app/vendor/cakephp/bake/psalm.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/vendor/cakephp/bake/rector.php b/app/vendor/cakephp/bake/rector.php
new file mode 100644
index 000000000..1aaa408d3
--- /dev/null
+++ b/app/vendor/cakephp/bake/rector.php
@@ -0,0 +1,81 @@
+withPaths([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+
+ ->withCache(
+ cacheClass: FileCacheStorage::class,
+ cacheDirectory: $cacheDir,
+ )
+
+ ->withPhpSets()
+ ->withAttributesSets()
+
+ ->withSets([
+ SetList::CODE_QUALITY,
+ SetList::CODING_STYLE,
+ SetList::DEAD_CODE,
+ SetList::EARLY_RETURN,
+ SetList::INSTANCEOF,
+ SetList::TYPE_DECLARATION,
+ ])
+
+ ->withSkip([
+ __DIR__ . '/tests/comparisons',
+ __DIR__ . '/tests/test_app',
+
+ ClassPropertyAssignToConstructorPromotionRector::class,
+ CatchExceptionNameMatchingTypeRector::class,
+ ClosureToArrowFunctionRector::class,
+ RemoveUselessReturnTagRector::class,
+ ReturnTypeFromStrictFluentReturnRector::class,
+ NewlineAfterStatementRector::class,
+ StringClassNameToClassConstantRector::class,
+ ReturnTypeFromStrictTypedCallRector::class,
+ ParamTypeByMethodCallTypeRector::class,
+ AddFunctionVoidReturnTypeWhereNoReturnRector::class,
+ StringableForToStringRector::class,
+ CompactToVariablesRector::class,
+ SplitDoubleAssignRector::class,
+ ChangeOrIfContinueToMultiContinueRector::class,
+ ExplicitBoolCompareRector::class,
+ NewlineBeforeNewAssignSetRector::class,
+ SimplifyEmptyCheckOnEmptyArrayRector::class,
+ DisallowedEmptyRuleFixerRector::class,
+ EncapsedStringsToSprintfRector::class,
+
+ // these causes problems with the testsuite
+ UseClassKeywordForClassNameResolutionRector::class,
+ FunctionFirstClassCallableRector::class,
+ ]);
diff --git a/app/vendor/cakephp/bake/src/BakePlugin.php b/app/vendor/cakephp/bake/src/BakePlugin.php
new file mode 100644
index 000000000..e2ce6b656
--- /dev/null
+++ b/app/vendor/cakephp/bake/src/BakePlugin.php
@@ -0,0 +1,154 @@
+getPlugins()->has('Cake/TwigView')) {
+ $app->addPlugin('Cake/TwigView');
+ }
+ }
+
+ /**
+ * Define the console commands for an application.
+ *
+ * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
+ * @return \Cake\Console\CommandCollection The updated collection.
+ */
+ public function console(CommandCollection $commands): CommandCollection
+ {
+ // Add commands in plugins and app.
+ $commands = $this->discoverCommands($commands);
+
+ // Add entry command to handle entry point and backwards compat.
+ $commands->add(EntryCommand::defaultName(), EntryCommand::class);
+
+ return $commands;
+ }
+
+ /**
+ * Scan plugins and application to find commands that are intended
+ * to be used with bake.
+ *
+ * Non-Abstract commands extending `Bake\Command\BakeCommand` are included.
+ * Plugins are scanned in the order they are listed in `Plugin::loaded()`
+ *
+ * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
+ * @return \Cake\Console\CommandCollection The updated collection.
+ */
+ protected function discoverCommands(CommandCollection $commands): CommandCollection
+ {
+ foreach (Plugin::getCollection()->with('console') as $plugin) {
+ $namespace = str_replace('/', '\\', $plugin->getName());
+ $pluginPath = $plugin->getClassPath();
+
+ $found = $this->findInPath($namespace, $pluginPath);
+ if ($found !== []) {
+ $commands->addMany($found);
+ }
+ }
+
+ $found = $this->findInPath(Configure::read('App.namespace'), APP);
+ if ($found !== []) {
+ $commands->addMany($found);
+ }
+
+ return $commands;
+ }
+
+ /**
+ * Search a path for commands.
+ *
+ * @param string $namespace The namespace classes are expected to be in.
+ * @param string $path The path to look in.
+ * @return array
+ * @phpstan-return array>
+ */
+ protected function findInPath(string $namespace, string $path): array
+ {
+ $hasSubfolder = false;
+ $path .= 'Command/';
+ $namespace .= '\Command\\';
+
+ if (file_exists($path . 'Bake/')) {
+ $hasSubfolder = true;
+ $path .= 'Bake/';
+ $namespace .= 'Bake\\';
+ } elseif (!file_exists($path)) {
+ return [];
+ }
+
+ $iterator = new DirectoryIterator($path);
+ $candidates = [];
+ foreach ($iterator as $item) {
+ if ($item->isDot() || $item->isDir()) {
+ continue;
+ }
+ $class = $namespace . $item->getBasename('.php');
+
+ if (!$hasSubfolder) {
+ if (!class_exists($class)) {
+ continue;
+ }
+
+ $reflection = new ReflectionClass($class);
+ if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(BakeCommand::class)) {
+ continue;
+ }
+ }
+
+ /** @var class-string<\Bake\Command\BakeCommand> $class */
+ $candidates[$class::defaultName()] = $class;
+ }
+
+ return $candidates;
+ }
+}
diff --git a/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php b/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php
index b543127c0..68e481e68 100644
--- a/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php
+++ b/app/vendor/cakephp/bake/src/CodeGen/ClassBuilder.php
@@ -18,10 +18,7 @@
class ClassBuilder
{
- /**
- * @var \Bake\CodeGen\ParsedClass|null
- */
- protected $parsedClass;
+ protected ?ParsedClass $parsedClass;
/**
* @param \Bake\CodeGen\ParsedClass $parsedClass Parsed class it already exists
@@ -50,7 +47,7 @@ public function getImplements(array $generated = []): array
*/
public function getUserConstants(array $generated = []): array
{
- if ($this->parsedClass === null) {
+ if (!$this->parsedClass instanceof ParsedClass) {
return [];
}
@@ -65,7 +62,7 @@ public function getUserConstants(array $generated = []): array
*/
public function getUserProperties(array $generated = []): array
{
- if ($this->parsedClass === null) {
+ if (!$this->parsedClass instanceof ParsedClass) {
return [];
}
@@ -80,7 +77,7 @@ public function getUserProperties(array $generated = []): array
*/
public function getUserFunctions(array $generated = []): array
{
- if ($this->parsedClass === null) {
+ if (!$this->parsedClass instanceof ParsedClass) {
return [];
}
diff --git a/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php b/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php
index 2293fb021..d3cc0e508 100644
--- a/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php
+++ b/app/vendor/cakephp/bake/src/CodeGen/CodeParser.php
@@ -17,17 +17,21 @@
namespace Bake\CodeGen;
use PhpParser\Error;
-use PhpParser\Lexer\Emulative;
use PhpParser\Node;
+use PhpParser\Node\Identifier;
+use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
-use PhpParser\Node\Stmt\UseUse;
+use PhpParser\Node\UseItem;
use PhpParser\NodeAbstract;
use PhpParser\NodeTraverser;
+use PhpParser\NodeVisitor;
use PhpParser\NodeVisitorAbstract;
+use PhpParser\Parser;
use PhpParser\ParserFactory;
+use PhpParser\PhpVersion;
/**
* @internal
@@ -39,37 +43,21 @@ class CodeParser extends NodeVisitorAbstract
*/
protected const INDENT = ' ';
- /**
- * @var \PhpParser\Parser
- */
- protected $parser;
+ protected Parser $parser;
- /**
- * @var \PhpParser\NodeTraverser
- */
- protected $traverser;
+ protected NodeTraverser $traverser;
- /**
- * @var string
- */
- protected $fileText = '';
+ protected string $fileText = '';
- /**
- * @var array
- */
- protected $parsed = [];
+ protected array $parsed = [];
/**
* Constructor
*/
public function __construct()
{
- $this->parser = (new ParserFactory())->create(
- ParserFactory::PREFER_PHP7,
- new Emulative([
- 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'],
- ])
- );
+ $version = PhpVersion::fromComponents(8, 1);
+ $this->parser = (new ParserFactory())->createForVersion($version);
$this->traverser = new NodeTraverser();
$this->traverser->addVisitor($this);
}
@@ -83,7 +71,11 @@ public function parseFile(string $code): ?ParsedFile
{
$this->fileText = $code;
try {
- $this->traverser->traverse($this->parser->parse($code));
+ $ast = $this->parser->parse($code);
+ if ($ast === null) {
+ return null;
+ }
+ $this->traverser->traverse($ast);
} catch (Error $e) {
throw new ParseException($e->getMessage(), null, $e);
}
@@ -97,7 +89,7 @@ public function parseFile(string $code): ?ParsedFile
$this->parsed['imports']['class'],
$this->parsed['imports']['function'],
$this->parsed['imports']['const'],
- $this->parsed['class']
+ $this->parsed['class'],
);
}
@@ -136,7 +128,11 @@ public function enterNode(Node $node)
throw new ParseException('Multiple use statements per line are not supported, update your file');
}
- [$alias, $target] = $this->normalizeUse(current($node->uses));
+ if ($node->uses === []) {
+ throw new ParseException('Use statement without uses!');
+ }
+
+ [$alias, $target] = $this->normalizeUse($node->uses[0]);
switch ($node->type) {
case Use_::TYPE_NORMAL:
$this->parsed['imports']['class'][$alias] = $target;
@@ -149,7 +145,7 @@ public function enterNode(Node $node)
break;
}
- return NodeTraverser::DONT_TRAVERSE_CHILDREN;
+ return NodeVisitor::DONT_TRAVERSE_CHILDREN;
}
if ($node instanceof GroupUse) {
@@ -170,7 +166,11 @@ public function enterNode(Node $node)
throw new ParseException('Multiple constants per line are not supported, update your file');
}
- $name = (string)current($constant->consts)->name;
+ $const = current($constant->consts);
+ if ($const === false) {
+ continue;
+ }
+ $name = (string)$const->name;
$constants[$name] = $this->getNodeCode($constant);
}
@@ -180,7 +180,11 @@ public function enterNode(Node $node)
throw new ParseException('Multiple properties per line are not supported, update your file');
}
- $name = (string)current($property->props)->name;
+ $prop = current($property->props);
+ if ($prop === false) {
+ continue;
+ }
+ $name = (string)$prop->name;
$properties[$name] = $this->getNodeCode($property);
}
@@ -190,7 +194,7 @@ public function enterNode(Node $node)
$methods[$name] = $this->getNodeCode($method);
}
- $implements = array_map(function ($name) {
+ $implements = array_map(function (Name $name): string {
return (string)$name;
}, $node->implements);
@@ -199,10 +203,10 @@ public function enterNode(Node $node)
$implements,
$constants,
$properties,
- $methods
+ $methods,
);
- return NodeTraverser::DONT_TRAVERSE_CHILDREN;
+ return NodeVisitor::DONT_TRAVERSE_CHILDREN;
}
return null;
@@ -223,17 +227,16 @@ protected function getNodeCode(NodeAbstract $node): string
$startPos = $node->getStartFilePos();
$endPos = $node->getEndFilePos();
- $code .= static::INDENT . substr($this->fileText, $startPos, $endPos - $startPos + 1);
- return $code;
+ return $code . static::INDENT . substr($this->fileText, $startPos, $endPos - $startPos + 1);
}
/**
- * @param \PhpParser\Node\Stmt\UseUse $use Use node
+ * @param \PhpParser\Node\UseItem $use Use item
* @param string|null $prefix Group use prefix
* @return array{string, string}
*/
- protected function normalizeUse(UseUse $use, ?string $prefix = null): array
+ protected function normalizeUse(UseItem $use, ?string $prefix = null): array
{
$name = (string)$use->name;
if ($prefix) {
@@ -241,13 +244,9 @@ protected function normalizeUse(UseUse $use, ?string $prefix = null): array
}
$alias = $use->alias;
- if (!$alias) {
+ if (!$alias instanceof Identifier) {
$last = strrpos($name, '\\', -1);
- if ($last !== false) {
- $alias = substr($name, strrpos($name, '\\', -1) + 1);
- } else {
- $alias = $name;
- }
+ $alias = $last !== false ? substr($name, strrpos($name, '\\', -1) + 1) : $name;
}
return [(string)$alias, $name];
diff --git a/app/vendor/cakephp/bake/src/CodeGen/ColumnTypeExtractor.php b/app/vendor/cakephp/bake/src/CodeGen/ColumnTypeExtractor.php
new file mode 100644
index 000000000..074cd530c
--- /dev/null
+++ b/app/vendor/cakephp/bake/src/CodeGen/ColumnTypeExtractor.php
@@ -0,0 +1,244 @@
+
+ */
+ protected array $columnTypes = [];
+
+ protected bool $inInitialize = false;
+
+ /**
+ * Constructor
+ */
+ public function __construct()
+ {
+ $version = PhpVersion::fromComponents(8, 1);
+ $this->parser = (new ParserFactory())->createForVersion($version);
+ }
+
+ /**
+ * Extracts column type mappings from initialize method code
+ *
+ * @param string $code The initialize method code
+ * @return array Map of column names to type expressions
+ */
+ public function extract(string $code): array
+ {
+ $this->columnTypes = [];
+ $this->inInitialize = false;
+
+ try {
+ // Wrap code in a dummy class if needed for parsing
+ $wrappedCode = "parser->parse($wrappedCode);
+ if ($ast === null) {
+ return [];
+ }
+
+ $traverser = new NodeTraverser();
+ $traverser->addVisitor($this);
+ $traverser->traverse($ast);
+ } catch (Exception) {
+ // If parsing fails, return empty array
+ return [];
+ }
+
+ return $this->columnTypes;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enterNode(Node $node)
+ {
+ // Check if we're entering the initialize method
+ if ($node instanceof Node\Stmt\ClassMethod && $node->name->name === 'initialize') {
+ $this->inInitialize = true;
+
+ return null;
+ }
+
+ // Only process nodes within initialize method
+ if (!$this->inInitialize) {
+ return null;
+ }
+
+ // Look for $this->getSchema()->setColumnType() calls
+ if ($node instanceof Expression && $node->expr instanceof MethodCall) {
+ $this->processMethodCall($node->expr);
+ } elseif ($node instanceof MethodCall) {
+ $this->processMethodCall($node);
+ }
+
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function leaveNode(Node $node)
+ {
+ if ($node instanceof Node\Stmt\ClassMethod && $node->name->name === 'initialize') {
+ $this->inInitialize = false;
+ }
+
+ return null;
+ }
+
+ /**
+ * Process a method call to check if it's setColumnType
+ *
+ * @param \PhpParser\Node\Expr\MethodCall $methodCall The method call to process
+ * @return void
+ */
+ protected function processMethodCall(MethodCall $methodCall): void
+ {
+ $isSetColumnTypeCall = $methodCall->name instanceof Node\Identifier
+ && $methodCall->name->name === 'setColumnType';
+ $schemaCall = $methodCall->var;
+ $isSchemaMethodCall = $schemaCall instanceof MethodCall;
+ $hasEnoughArguments = count($methodCall->args) >= 2;
+
+ if (!$isSetColumnTypeCall || !$isSchemaMethodCall || !$hasEnoughArguments) {
+ return;
+ }
+
+ $isGetSchemaCall = $schemaCall->name instanceof Node\Identifier
+ && $schemaCall->name->name === 'getSchema';
+ $isCalledOnThis = $schemaCall->var instanceof Variable
+ && $schemaCall->var->name === 'this';
+
+ if (!$isGetSchemaCall || !$isCalledOnThis) {
+ return;
+ }
+
+ $columnArgNode = $methodCall->args[0];
+ $typeArgNode = $methodCall->args[1];
+ if (!$columnArgNode instanceof Node\Arg || !$typeArgNode instanceof Node\Arg) {
+ return;
+ }
+
+ $columnArg = $columnArgNode->value;
+ $typeArg = $typeArgNode->value;
+
+ $columnName = $this->getStringValue($columnArg);
+ if ($columnName === null) {
+ return;
+ }
+
+ $typeExpression = $this->getTypeExpression($typeArg);
+ if ($typeExpression !== null) {
+ $this->columnTypes[$columnName] = $typeExpression;
+ }
+ }
+
+ /**
+ * Get string value from a node
+ *
+ * @param \PhpParser\Node $node The node to extract string from
+ * @return string|null The string value or null
+ */
+ protected function getStringValue(Node $node): ?string
+ {
+ if ($node instanceof Node\Scalar\String_) {
+ return $node->value;
+ }
+
+ return null;
+ }
+
+ /**
+ * Convert a type expression node to string representation
+ *
+ * @param \PhpParser\Node $node The type expression node
+ * @return string|null String representation of the type expression
+ */
+ protected function getTypeExpression(Node $node): ?string
+ {
+ if ($node instanceof Node\Expr\StaticCall) {
+ $staticCall = $node;
+ $calledClass = $staticCall->class;
+ $calledMethod = $staticCall->name;
+
+ $hasNamedClass = $calledClass instanceof Node\Name;
+ $hasIdentifierMethod = $calledMethod instanceof Node\Identifier;
+ if (!$hasNamedClass || !$hasIdentifierMethod) {
+ return null;
+ }
+
+ $className = $calledClass->toString();
+ $methodName = $calledMethod->name;
+ $isEnumTypeClass = $className === 'EnumType' || str_ends_with($className, '\\EnumType');
+ $isFromMethod = $methodName === 'from';
+ $hasArguments = $staticCall->args !== [];
+ if (!$isEnumTypeClass || !$isFromMethod || !$hasArguments) {
+ return null;
+ }
+
+ $argNode = $staticCall->args[0];
+ if (!$argNode instanceof Node\Arg) {
+ return null;
+ }
+
+ $arg = $argNode->value;
+ if (!$arg instanceof Node\Expr\ClassConstFetch) {
+ return null;
+ }
+
+ $enumClassNode = $arg->class;
+ $constantName = $arg->name;
+ $hasNamedEnumClass = $enumClassNode instanceof Node\Name;
+ $isClassConstant = $constantName instanceof Node\Identifier
+ && $constantName->name === 'class';
+ if (!$hasNamedEnumClass || !$isClassConstant) {
+ return null;
+ }
+
+ $enumClass = $enumClassNode->toString();
+
+ return 'EnumType::from(' . $enumClass . '::class)';
+ }
+
+ if ($node instanceof Node\Scalar\String_) {
+ return '"' . $node->value . '"';
+ }
+
+ return null;
+ }
+}
diff --git a/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php b/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php
index 74c77a7aa..ece3c8b11 100644
--- a/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php
+++ b/app/vendor/cakephp/bake/src/CodeGen/FileBuilder.php
@@ -20,25 +20,13 @@
class FileBuilder
{
- /**
- * @var \Cake\Console\ConsoleIo
- */
- protected $io;
+ protected ConsoleIo $io;
- /**
- * @var string
- */
- protected $namespace;
+ protected string $namespace;
- /**
- * @var \Bake\CodeGen\ParsedFile|null
- */
- protected $parsedFile;
+ protected ?ParsedFile $parsedFile;
- /**
- * @var \Bake\CodeGen\ClassBuilder
- */
- protected $classBuilder;
+ protected ClassBuilder $classBuilder;
/**
* @param \Cake\Console\ConsoleIo $io Console io
@@ -47,11 +35,11 @@ class FileBuilder
*/
public function __construct(ConsoleIo $io, string $namespace, ?ParsedFile $parsedFile = null)
{
- if ($parsedFile && $parsedFile->namespace !== $namespace) {
+ if ($parsedFile instanceof ParsedFile && $parsedFile->namespace !== $namespace) {
throw new ParseException(sprintf(
'Existing namespace `%s` does not match expected namespace `%s`, cannot update existing file',
$parsedFile->namespace,
- $namespace
+ $namespace,
));
}
diff --git a/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php b/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php
index 576b42c7b..40308c90d 100644
--- a/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php
+++ b/app/vendor/cakephp/bake/src/CodeGen/ImportHelper.php
@@ -32,11 +32,7 @@ public static function normalize(array $imports): array
foreach ($imports as $alias => $class) {
if (is_int($alias)) {
$last = strrpos($class, '\\', -1);
- if ($last !== false) {
- $alias = substr($class, strrpos($class, '\\', -1) + 1);
- } else {
- $alias = $class;
- }
+ $alias = $last !== false ? substr($class, strrpos($class, '\\', -1) + 1) : $class;
}
$normalized[$alias] = $class;
@@ -58,21 +54,21 @@ public static function merge(array $existing, array $imports, ?ConsoleIo $io = n
$existing = static::normalize($existing);
foreach (static::normalize($imports) as $alias => $class) {
if (isset($existing[$alias]) && $existing[$alias] !== $class) {
- if ($io) {
+ if ($io instanceof ConsoleIo) {
$io->warning(sprintf(
'Import `%s` conflicts with existing import, discarding.',
- $class
+ $class,
));
}
continue;
}
$existingAlias = array_search($class, $existing, true);
- if ($existingAlias !== false && $existingAlias != $alias) {
- if ($io) {
+ if ($existingAlias !== false && $existingAlias !== $alias) {
+ if ($io instanceof ConsoleIo) {
$io->warning(sprintf(
'Import `%s` conflicts with existing import, discarding.',
- $class
+ $class,
));
}
continue;
diff --git a/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php b/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php
index b7bda0737..28cad4ded 100644
--- a/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php
+++ b/app/vendor/cakephp/bake/src/CodeGen/ParsedClass.php
@@ -21,30 +21,27 @@
*/
class ParsedClass
{
- /**
- * @var string
- */
- public $name;
+ public string $name;
/**
* @var array
*/
- public $implements;
+ public array $implements;
/**
* @var array
*/
- public $constants;
+ public array $constants;
/**
* @var array
*/
- public $properties;
+ public array $properties;
/**
* @var array
*/
- public $methods;
+ public array $methods;
/**
* @param string $name Class name
diff --git a/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php b/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php
index f00521710..185032bf6 100644
--- a/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php
+++ b/app/vendor/cakephp/bake/src/CodeGen/ParsedFile.php
@@ -21,30 +21,24 @@
*/
class ParsedFile
{
- /**
- * @var string
- */
- public $namespace;
+ public string $namespace;
/**
* @var array
*/
- public $classImports;
+ public array $classImports;
/**
* @var array
*/
- public $functionImports;
+ public array $functionImports;
/**
* @var array
*/
- public $constImports;
+ public array $constImports;
- /**
- * @var \Bake\CodeGen\ParsedClass
- */
- public $class;
+ public ParsedClass $class;
/**
* @param string $namespace Namespace
@@ -58,7 +52,7 @@ public function __construct(
array $classImports,
array $functionImports,
array $constImports,
- ParsedClass $class
+ ParsedClass $class,
) {
$this->namespace = $namespace;
$this->classImports = $classImports;
diff --git a/app/vendor/cakephp/bake/src/Command/AllCommand.php b/app/vendor/cakephp/bake/src/Command/AllCommand.php
index 785f46534..cd0a5993e 100644
--- a/app/vendor/cakephp/bake/src/Command/AllCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/AllCommand.php
@@ -21,6 +21,7 @@
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Datasource\ConnectionManager;
+use Throwable;
/**
* Command for `bake all`
@@ -30,9 +31,9 @@ class AllCommand extends BakeCommand
/**
* All commands to call.
*
- * @var string[]
+ * @var array
*/
- protected $commands = [
+ protected array $commands = [
ModelCommand::class,
ControllerCommand::class,
TemplateCommand::class,
@@ -44,12 +45,12 @@ class AllCommand extends BakeCommand
* @param \Cake\Console\ConsoleOptionParser $parser Option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser = $parser->setDescription(
- 'Generate the model, controller, template, tests and fixture for a table.'
+ 'Generate the model, controller, template, tests and fixture for a table.',
)->addArgument('name', [
'help' => 'Name of the table to generate code for.',
])->addOption('everything', [
@@ -83,20 +84,21 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
/** @var \Cake\Database\Connection $connection */
$connection = ConnectionManager::get($this->connection);
$scanner = new TableScanner($connection);
- if (empty($name) && !$args->getOption('everything')) {
+ $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped());
+
+ if (!$name && !$args->getOption('everything')) {
$io->out('Choose a table to generate from the following:');
- foreach ($scanner->listUnskipped() as $table) {
+ foreach ($tables as $table) {
$io->out('- ' . $this->_camelize($table));
}
return static::CODE_SUCCESS;
}
- if ($args->getOption('everything')) {
- $tables = $scanner->listUnskipped();
- } else {
+ if (!$args->getOption('everything')) {
$tables = [$name];
}
+ $errors = 0;
foreach ($this->commands as $commandName) {
/** @var \Cake\Command\Command $command */
$command = new $commandName();
@@ -111,13 +113,29 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
}
foreach ($tables as $table) {
- $subArgs = new Arguments([$table], $options, ['name']);
- $command->execute($subArgs, $io);
+ $parser = $command->getOptionParser();
+ $subArgs = new Arguments([$table], $options, $parser->argumentNames());
+
+ try {
+ $command->execute($subArgs, $io);
+ } catch (Throwable $e) {
+ if (!$args->getOption('everything') || !$args->getOption('force')) {
+ throw $e;
+ }
+
+ $message = sprintf('Error generating %s for %s: %s', $commandName, $table, $e->getMessage());
+ $io->error($message);
+ $errors++;
+ }
}
}
- $io->out('Bake All complete. ', 1, ConsoleIo::NORMAL);
+ if ($errors) {
+ $io->warning(sprintf('Bake All completed, but with %s errors.', $errors));
+ } else {
+ $io->success('Bake All complete.');
+ }
- return static::CODE_SUCCESS;
+ return $errors ? static::CODE_ERROR : static::CODE_SUCCESS;
}
}
diff --git a/app/vendor/cakephp/bake/src/Command/BakeCommand.php b/app/vendor/cakephp/bake/src/Command/BakeCommand.php
index 06e84064a..1c783b2e6 100644
--- a/app/vendor/cakephp/bake/src/Command/BakeCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/BakeCommand.php
@@ -27,7 +27,9 @@
use Cake\Core\ConventionsTrait;
use Cake\Event\Event;
use Cake\Event\EventManager;
+use Cake\ORM\Locator\TableLocator;
use InvalidArgumentException;
+use function Cake\Core\pluginSplit;
/**
* Base class for commands that bake can use.
@@ -42,10 +44,24 @@ abstract class BakeCommand extends Command
/**
* The pathFragment appended to the plugin/app path.
+ */
+ protected string $pathFragment;
+
+ /**
+ * Initialize the command.
*
- * @var string
+ * @return void
*/
- protected $pathFragment;
+ public function initialize(): void
+ {
+ parent::initialize();
+
+ $locator = $this->getTableLocator();
+ if ($locator instanceof TableLocator) {
+ $locator->allowFallbackClass(true);
+ $this->setTableLocator($locator);
+ }
+ }
/**
* Get the command name.
@@ -59,7 +75,7 @@ abstract class BakeCommand extends Command
public static function defaultName(): string
{
$name = parent::defaultName();
- if (strpos($name, 'bake_') === 0) {
+ if (str_starts_with($name, 'bake_')) {
$name = substr($name, 5);
}
@@ -94,13 +110,14 @@ protected function _getName(string $name): string
*/
protected function getPrefix(Arguments $args): string
{
+ /** @var string|null $prefix */
$prefix = $args->getOption('prefix');
if (!$prefix) {
return '';
}
$parts = explode('/', $prefix);
- return implode('/', array_map([$this, '_camelize'], $parts));
+ return implode('/', array_map($this->_camelize(...), $parts));
}
/**
@@ -137,7 +154,7 @@ public function getTemplatePath(Arguments $args, ?string $container = null): str
if (empty($paths)) {
throw new InvalidArgumentException(
'Could not read template paths. ' .
- 'Ensure `App.paths.templates` is defined in your application configuration.'
+ 'Ensure `App.paths.templates` is defined in your application configuration.',
);
}
$path = $paths[0];
@@ -179,7 +196,7 @@ protected function deleteEmptyFile(string $path, ConsoleIo $io): void
{
if (file_exists($path)) {
unlink($path);
- $io->out(sprintf('Deleted `%s`', $path), 1, ConsoleIo::NORMAL);
+ $io->out(sprintf('Deleted `%s`', $path));
}
}
@@ -195,7 +212,7 @@ protected function deleteEmptyFile(string $path, ConsoleIo $io): void
*/
protected function isValidColumnName(string $name): bool
{
- return (bool)preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name);
+ return (bool)preg_match('/^[a-zA-Z_]\w*$/', $name);
}
/**
@@ -207,7 +224,12 @@ protected function isValidColumnName(string $name): bool
protected function parseFile(string $path): ?ParsedFile
{
if (file_exists($path)) {
- return (new CodeParser())->parseFile(file_get_contents($path));
+ $contents = file_get_contents($path);
+ if ($contents === false) {
+ return null;
+ }
+
+ return (new CodeParser())->parseFile($contents);
}
return null;
@@ -220,7 +242,7 @@ protected function parseFile(string $path): ?ParsedFile
* @param string $path The path to create the file at
* @param string $contents The contents to put into the file
* @param bool $forceOverwrite Whether the file should be overwritten without prompting the user
- * @param bool $skipIfUnchnged Skip writing output if the contents match existing file
+ * @param bool $skipIfUnchanged Skip writing output if the contents match existing file
* @return bool True if successful, false otherwise
* @throws \Cake\Console\Exception\StopException When `q` is given as an answer
* to whether a file should be overwritten.
@@ -230,9 +252,9 @@ protected function writeFile(
string $path,
string $contents,
bool $forceOverwrite = false,
- bool $skipIfUnchnged = true
+ bool $skipIfUnchanged = true,
): bool {
- if ($skipIfUnchnged && file_exists($path) && file_get_contents($path) === $contents) {
+ if ($skipIfUnchanged && file_exists($path) && file_get_contents($path) === $contents) {
$io->info("Skipping update to `{$path}`. It already exists and would not change.");
return true;
diff --git a/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php b/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php
index c7af462f9..c4a06a330 100644
--- a/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/BehaviorCommand.php
@@ -23,10 +23,8 @@ class BehaviorCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Model/Behavior/';
+ public string $pathFragment = 'Model/Behavior/';
/**
* @inheritDoc
diff --git a/app/vendor/cakephp/bake/src/Command/CellCommand.php b/app/vendor/cakephp/bake/src/Command/CellCommand.php
index b77be780f..c0fb6dc5f 100644
--- a/app/vendor/cakephp/bake/src/Command/CellCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/CellCommand.php
@@ -28,10 +28,8 @@ class CellCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'View/Cell/';
+ public string $pathFragment = 'View/Cell/';
/**
* @inheritDoc
@@ -87,7 +85,7 @@ public function templateData(Arguments $arguments): array
* @param \Cake\Console\ConsoleIo $io The console io
* @return void
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
$this->bakeTemplate($name, $args, $io);
@@ -116,7 +114,7 @@ protected function bakeTemplate(string $name, Arguments $args, ConsoleIo $io): v
* @param \Cake\Console\ConsoleOptionParser $parser Parser instance
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = parent::buildOptionParser($parser);
$parser->addOption('prefix', [
diff --git a/app/vendor/cakephp/bake/src/Command/CommandCommand.php b/app/vendor/cakephp/bake/src/Command/CommandCommand.php
index ea5cabd8e..8612f330c 100644
--- a/app/vendor/cakephp/bake/src/Command/CommandCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/CommandCommand.php
@@ -16,6 +16,9 @@
*/
namespace Bake\Command;
+use Cake\Console\Arguments;
+use Cake\Utility\Inflector;
+
/**
* Console Command generator.
*/
@@ -23,10 +26,8 @@ class CommandCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Command/';
+ public string $pathFragment = 'Command/';
/**
* @inheritDoc
@@ -51,4 +52,24 @@ public function template(): string
{
return 'Bake.Command/command';
}
+
+ /**
+ * Get template data.
+ *
+ * @param \Cake\Console\Arguments $arguments Arguments object.
+ * @return array
+ * @phpstan-return array
+ */
+ public function templateData(Arguments $arguments): array
+ {
+ $data = parent::templateData($arguments);
+
+ $data['command_name'] = Inflector::underscore(str_replace(
+ '.',
+ ' ',
+ $arguments->getArgument('name') ?? '',
+ ));
+
+ return $data;
+ }
}
diff --git a/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php b/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php
index 84f8d63bd..643ed758a 100644
--- a/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/CommandHelperCommand.php
@@ -23,10 +23,8 @@ class CommandHelperCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Command/Helper/';
+ public string $pathFragment = 'Command/Helper/';
/**
* @inheritDoc
diff --git a/app/vendor/cakephp/bake/src/Command/ComponentCommand.php b/app/vendor/cakephp/bake/src/Command/ComponentCommand.php
index 452800fde..af1bbe80f 100644
--- a/app/vendor/cakephp/bake/src/Command/ComponentCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/ComponentCommand.php
@@ -23,10 +23,8 @@ class ComponentCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Controller/Component/';
+ public string $pathFragment = 'Controller/Component/';
/**
* @inheritDoc
diff --git a/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php b/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php
index b482fcce6..43ffb3ca4 100644
--- a/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/ControllerAllCommand.php
@@ -30,10 +30,7 @@ class ControllerAllCommand extends BakeCommand
{
use LocatorAwareTrait;
- /**
- * @var \Bake\Command\ControllerCommand
- */
- protected $controllerCommand;
+ protected ControllerCommand $controllerCommand;
/**
* @inheritDoc
@@ -83,7 +80,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
* @param \Cake\Console\ConsoleOptionParser $parser The console option parser
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->controllerCommand->buildOptionParser($parser);
$parser
diff --git a/app/vendor/cakephp/bake/src/Command/ControllerCommand.php b/app/vendor/cakephp/bake/src/Command/ControllerCommand.php
index 2b4047f70..675a255ee 100644
--- a/app/vendor/cakephp/bake/src/Command/ControllerCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/ControllerCommand.php
@@ -21,6 +21,7 @@
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
+use Cake\Core\Plugin;
use Cake\Datasource\ConnectionManager;
/**
@@ -30,10 +31,8 @@ class ControllerCommand extends BakeCommand
{
/**
* Path fragment for generated code.
- *
- * @var string
*/
- public $pathFragment = 'Controller/';
+ public string $pathFragment = 'Controller/';
/**
* Execute the command.
@@ -83,9 +82,12 @@ public function bake(string $controllerName, Arguments $args, ConsoleIo $io): vo
$actions = ['index', 'view', 'add', 'edit', 'delete'];
}
if ($args->getOption('actions')) {
- $actions = array_map('trim', explode(',', $args->getOption('actions')));
+ $actions = array_map('trim', explode(',', (string)$args->getOption('actions')));
$actions = array_filter($actions);
}
+ if (!$args->getOption('actions') && Plugin::isLoaded('Authentication') && $controllerName === 'Users') {
+ $actions[] = 'login';
+ }
$helpers = $this->getHelpers($args);
$components = $this->getComponents($args);
@@ -125,6 +127,12 @@ public function bake(string $controllerName, Arguments $args, ConsoleIo $io): vo
$singularHumanName = $this->_singularHumanName($controllerName);
$pluralHumanName = $this->_variableName($controllerName);
+ // Handle cases where singular and plural are identical (e.g., "news", "sheep")
+ // to avoid variable collisions in generated controller code
+ if ($singularName === $pluralName) {
+ $singularName .= 'Entity';
+ }
+
$defaultModel = sprintf('%s\Model\Table\%sTable', $namespace, $controllerName);
if (!class_exists($defaultModel)) {
$defaultModel = null;
@@ -146,7 +154,7 @@ public function bake(string $controllerName, Arguments $args, ConsoleIo $io): vo
'pluralName',
'prefix',
'singularHumanName',
- 'singularName'
+ 'singularName',
);
$data['name'] = $controllerName;
@@ -202,7 +210,7 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi
$testArgs = new Arguments(
['controller', $className],
$args->getOptions(),
- ['type', 'name']
+ ['type', 'name'],
);
$test->execute($testArgs, $io);
}
@@ -211,14 +219,16 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi
* Get the list of components for the controller.
*
* @param \Cake\Console\Arguments $args The console arguments
- * @return string[]
+ * @return array
*/
public function getComponents(Arguments $args): array
{
$components = [];
if ($args->getOption('components')) {
- $components = explode(',', $args->getOption('components'));
+ $components = explode(',', (string)$args->getOption('components'));
$components = array_values(array_filter(array_map('trim', $components)));
+ } elseif (Plugin::isLoaded('Authorization')) {
+ $components[] = 'Authorization.Authorization';
}
return $components;
@@ -228,13 +238,13 @@ public function getComponents(Arguments $args): array
* Get the list of helpers for the controller.
*
* @param \Cake\Console\Arguments $args The console arguments
- * @return string[]
+ * @return array
*/
public function getHelpers(Arguments $args): array
{
$helpers = [];
if ($args->getOption('helpers')) {
- $helpers = explode(',', $args->getOption('helpers'));
+ $helpers = explode(',', (string)$args->getOption('helpers'));
$helpers = array_values(array_filter(array_map('trim', $helpers)));
}
@@ -247,11 +257,11 @@ public function getHelpers(Arguments $args): array
* @param \Cake\Console\ConsoleOptionParser $parser The console option parser
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser->setDescription(
- 'Bake a controller skeleton.'
+ 'Bake a controller skeleton.',
)->addArgument('name', [
'help' => 'Name of the controller to bake (without the `Controller` suffix). ' .
'You can use Plugin.name to bake controllers into plugins.',
diff --git a/app/vendor/cakephp/bake/src/Command/EntryCommand.php b/app/vendor/cakephp/bake/src/Command/EntryCommand.php
index c549880e9..7f8490ad4 100644
--- a/app/vendor/cakephp/bake/src/Command/EntryCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/EntryCommand.php
@@ -16,7 +16,6 @@
*/
namespace Bake\Command;
-use Bake\Shell\Task\BakeTask;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\Command\HelpCommand;
@@ -25,10 +24,6 @@
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Console\Exception\ConsoleException;
-use Cake\Console\Shell;
-use Cake\Core\Configure;
-use Cake\Core\Plugin as CorePlugin;
-use Cake\Utility\Inflector;
/**
* Command that provides help and an entry point to bake tools.
@@ -37,17 +32,13 @@ class EntryCommand extends Command implements CommandCollectionAwareInterface
{
/**
* The command collection to get help on.
- *
- * @var \Cake\Console\CommandCollection
*/
- protected $commands;
+ protected CommandCollection $commands;
/**
* The HelpCommand to get help.
- *
- * @var \Cake\Console\Command\HelpCommand
*/
- protected $help;
+ protected HelpCommand $help;
/**
* @inheritDoc
@@ -68,8 +59,7 @@ public function setCommandCollection(CommandCollection $commands): void
/**
* Run the command.
*
- * Override the run() method so that we can splice in dynamic
- * subcommand handling for legacy tasks.
+ * Override the run() method for special handling of the `--help` option.
*
* @param array $argv Arguments from the CLI environment.
* @param \Cake\Console\ConsoleIo $io The console io
@@ -85,10 +75,10 @@ public function run(array $argv, ConsoleIo $io): ?int
$args = new Arguments(
$arguments,
$options,
- $parser->argumentNames()
+ $parser->argumentNames(),
);
} catch (ConsoleException $e) {
- $io->err('Error: ' . $e->getMessage());
+ $io->error('Error: ' . $e->getMessage());
return static::CODE_ERROR;
}
@@ -107,9 +97,6 @@ public function run(array $argv, ConsoleIo $io): ?int
/**
* Execute the command.
*
- * This command acts as a catch-all for legacy tasks that may
- * be defined in the application or plugins.
- *
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
* @return int|null The exit code or null for success
@@ -118,105 +105,42 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
{
if ($args->hasArgumentAt(0)) {
$name = $args->getArgumentAt(0);
- $task = $this->createTask($name, $io);
- if ($task) {
- $argList = $args->getArguments();
-
- // Remove command name.
- array_shift($argList);
- foreach ($args->getOptions() as $key => $value) {
- if ($value === false) {
- continue;
- } elseif ($value === true) {
- $argList[] = '--' . $key;
- } else {
- $argList[] = '--' . $key;
- $argList[] = $value;
- }
- }
-
- $result = $task->runCommand($argList);
- if ($result === false) {
- return static::CODE_ERROR;
- }
- if ($result === true) {
- return static::CODE_SUCCESS;
- }
-
- return $result;
- }
- $io->err("Could not find a task named `{$name}`. ");
+ $io->error(
+ "Could not find bake command named `{$name}`."
+ . ' Run `bake --help` to get a list of commands.',
+ );
return static::CODE_ERROR;
}
- $io->err('No command provided. Run `bake --help` to get a list of commands. ');
+ $io->warning('No command provided. Run `bake --help` to get a list of commands.');
return static::CODE_ERROR;
}
- /**
- * Find and create a Shell based BakeTask
- *
- * @param string $name The task name.
- * @param \Cake\Console\ConsoleIo $io The console io.
- * @return \Cake\Console\Shell|null
- */
- protected function createTask(string $name, ConsoleIo $io): ?Shell
- {
- $found = false;
- $name = Inflector::camelize($name);
- $factory = function ($className, $io) {
- $task = new $className($io);
- $task->setRootName('cake bake');
-
- return $task;
- };
-
- // Look in each plugin for the requested task
- foreach (CorePlugin::loaded() as $plugin) {
- $namespace = str_replace('/', '\\', $plugin);
- $candidate = $namespace . '\Shell\Task\\' . $name . 'Task';
- if (class_exists($candidate) && is_subclass_of($candidate, BakeTask::class)) {
- return $factory($candidate, $io);
- }
- }
-
- // Try the app as well
- $namespace = Configure::read('App.namespace');
- $candidate = $namespace . '\Shell\Task\\' . $name . 'Task';
- if (class_exists($candidate) && is_subclass_of($candidate, BakeTask::class)) {
- return $factory($candidate, $io);
- }
-
- return null;
- }
-
/**
* Gets the option parser instance and configures it.
*
* @param \Cake\Console\ConsoleOptionParser $parser The console option parser
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$this->help = new HelpCommand();
- /** @psalm-suppress InaccessibleMethod Protected methods as class based */
$parser = $this->help->buildOptionParser($parser);
$parser
->setDescription(
'Bake generates code for your application. Different types of classes can be generated' .
' with the subcommands listed below. For example run bake controller --help ' .
- ' to learn more about generating a controller.'
- )
- ->setEpilog('Older Shell based tasks will not be listed here, but can still be run.');
+ ' to learn more about generating a controller.',
+ );
$commands = [];
foreach ($this->commands as $command => $class) {
- if (substr($command, 0, 4) === 'bake') {
+ if (str_starts_with($command, 'bake')) {
$parts = explode(' ', $command);
// Remove `bake`
array_shift($parts);
- if (count($parts) === 0) {
+ if ($parts === []) {
continue;
}
$commands[$command] = $class;
diff --git a/app/vendor/cakephp/bake/src/Command/EnumCommand.php b/app/vendor/cakephp/bake/src/Command/EnumCommand.php
new file mode 100644
index 000000000..5eee0d0ef
--- /dev/null
+++ b/app/vendor/cakephp/bake/src/Command/EnumCommand.php
@@ -0,0 +1,171 @@
+
+ */
+ public function templateData(Arguments $arguments): array
+ {
+ $cases = EnumParser::parseCases($arguments->getArgument('cases'), (bool)$arguments->getOption('int'));
+ $isOfTypeInt = $this->isOfTypeInt($cases);
+ $backingType = $isOfTypeInt ? 'int' : 'string';
+ if ($arguments->getOption('int')) {
+ if ($cases && !$isOfTypeInt) {
+ throw new InvalidArgumentException('Cases do not match requested `int` backing type.');
+ }
+
+ $backingType = 'int';
+ }
+
+ $data = parent::templateData($arguments);
+ $data['backingType'] = $backingType;
+ $data['cases'] = $this->formatCases($cases);
+
+ return $data;
+ }
+
+ /**
+ * Gets the option parser instance and configures it.
+ *
+ * @param \Cake\Console\ConsoleOptionParser $parser The option parser to update.
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ {
+ $parser = $this->_setCommonOptions($parser);
+
+ $parser->setDescription(
+ 'Bake backed enums for use in models.',
+ )->addArgument('name', [
+ 'help' => 'Name of the enum to bake. You can use Plugin.name to bake plugin enums.',
+ 'required' => true,
+ ])->addArgument('cases', [
+ 'help' => 'List of either `one,two` for string or `foo:0,bar:1` for int type.',
+ ])->addOption('int', [
+ 'help' => 'Using backed enums with int instead of string as return type.',
+ 'boolean' => true,
+ 'short' => 'i',
+ ]);
+
+ return $parser;
+ }
+
+ /**
+ * @param array $definition
+ * @return bool
+ */
+ protected function isOfTypeInt(array $definition): bool
+ {
+ if (!$definition) {
+ return false;
+ }
+
+ foreach ($definition as $value) {
+ if (!is_int($value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param array $cases
+ * @return array
+ */
+ protected function formatCases(array $cases): array
+ {
+ $formatted = [];
+ foreach ($cases as $case => $value) {
+ $case = Inflector::camelize(Inflector::underscore($case));
+ if (is_string($value)) {
+ $value = "'" . $value . "'";
+ }
+ $formatted[] = 'case ' . $case . ' = ' . $value . ';';
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * Generate a class stub
+ *
+ * @param string $name The class name
+ * @param \Cake\Console\Arguments $args The console arguments
+ * @param \Cake\Console\ConsoleIo $io The console io
+ * @return void
+ */
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
+ {
+ parent::bake($name, $args, $io);
+
+ $path = $this->getPath($args);
+ $filename = $path . $name . '.php';
+
+ // Work around composer caching that classes/files do not exist.
+ // Check for the file as it might not exist in tests.
+ if (file_exists($filename)) {
+ require_once $filename;
+ }
+ }
+}
diff --git a/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php b/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php
index 9dff89cd0..241e7277c 100644
--- a/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/FixtureAllCommand.php
@@ -44,16 +44,16 @@ public static function defaultName(): string
* @param \Cake\Console\ConsoleOptionParser $parser The parser to update
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser = $parser->setDescription(
- 'Generate all fixtures for use with the test suite.'
+ 'Generate all fixtures for use with the test suite.',
)->addOption('count', [
'help' => 'When using generated data, the number of records to include in the fixture(s).',
'short' => 'n',
- 'default' => 1,
+ 'default' => '1',
])->addOption('schema', [
'help' => 'Create a fixture that imports schema, instead of dumping a schema snapshot into the fixture.',
'short' => 's',
@@ -83,10 +83,12 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$this->extractCommonProperties($args);
/** @var \Cake\Database\Connection $connection */
- $connection = ConnectionManager::get($args->getOption('connection') ?? 'default');
+ $connection = ConnectionManager::get((string)($args->getOption('connection') ?: 'default'));
$scanner = new TableScanner($connection);
$fixture = new FixtureCommand();
- foreach ($scanner->listUnskipped() as $table) {
+
+ $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped());
+ foreach ($tables as $table) {
$fixtureArgs = new Arguments([$table], $args->getOptions(), ['name']);
$fixture->execute($fixtureArgs, $io);
}
diff --git a/app/vendor/cakephp/bake/src/Command/FixtureCommand.php b/app/vendor/cakephp/bake/src/Command/FixtureCommand.php
index 402b092e8..1ae33678d 100644
--- a/app/vendor/cakephp/bake/src/Command/FixtureCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/FixtureCommand.php
@@ -18,16 +18,21 @@
use Bake\Utility\TableScanner;
use Brick\VarExporter\VarExporter;
+use Cake\Chronos\Chronos;
+use Cake\Chronos\ChronosDate;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Core\Configure;
-use Cake\Database\Exception\DatabaseException;
+use Cake\Core\Exception\CakeException;
use Cake\Database\Schema\TableSchemaInterface;
+use Cake\Database\Type\EnumType;
+use Cake\Database\TypeFactory;
use Cake\Datasource\ConnectionManager;
use Cake\Utility\Inflector;
use Cake\Utility\Text;
use DateTimeInterface;
+use ReflectionEnum;
/**
* Task class for creating and updating fixtures files.
@@ -57,12 +62,12 @@ public function getPath(Arguments $args): string
* @param \Cake\Console\ConsoleOptionParser $parser Option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser = $parser->setDescription(
- 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.'
+ 'Generate fixtures for use with the test suite. You can use `bake fixture all` to bake all fixtures.',
)->addArgument('name', [
'help' => 'Name of the fixture to bake (without the `Fixture` suffix). ' .
'You can use Plugin.name to bake plugin fixtures.',
@@ -71,10 +76,9 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
])->addOption('count', [
'help' => 'When using generated data, the number of records to include in the fixture(s).',
'short' => 'n',
- 'default' => 1,
+ 'default' => '1',
])->addOption('fields', [
'help' => 'Create a fixture that includes the deprecated $fields property.',
- 'short' => 'f',
'boolean' => true,
])->addOption('schema', [
'help' => 'Create a fixture that imports schema, instead of dumping a schema snapshot into the fixture.',
@@ -118,7 +122,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
return static::CODE_SUCCESS;
}
- $table = $args->getOption('table') ?? '';
+ $table = (string)$args->getOption('table');
$model = $this->_camelize($name);
$this->bake($model, $table, $args, $io);
@@ -159,7 +163,7 @@ protected function bake(string $model, string $useTable, Arguments $args, Consol
try {
$data = $this->readSchema($model, $useTable);
- } catch (DatabaseException $e) {
+ } catch (CakeException) {
$this->getTableLocator()->remove($model);
$useTable = Inflector::underscore($model);
$table = $useTable;
@@ -223,7 +227,7 @@ public function validateNames(TableSchemaInterface $schema, ConsoleIo $io): void
$io->abort(sprintf(
'Unable to bake model. Table column name must start with a letter or underscore and
cannot contain special characters. Found `%s`.',
- $column
+ $column,
));
}
}
@@ -265,7 +269,7 @@ public function generateFixtureFile(Arguments $args, ConsoleIo $io, string $mode
->set($vars)
->generate('Bake.tests/fixture');
- $io->out("\n" . sprintf('Baking test fixture for %s...', $model), 1, ConsoleIo::NORMAL);
+ $io->out("\n" . sprintf('Baking test fixture for %s...', $model));
$io->createFile($path . $filename, $contents, $this->force);
$emptyFile = $path . '.gitkeep';
$this->deleteEmptyFile($emptyFile, $io);
@@ -281,19 +285,22 @@ protected function _generateSchema(TableSchemaInterface $table): string
{
$cols = $indexes = $constraints = [];
foreach ($table->columns() as $field) {
+ /** @var array $fieldData */
$fieldData = $table->getColumn($field);
$properties = implode(', ', $this->_values($fieldData));
- $cols[] = " '$field' => [$properties],";
+ $cols[] = " '{$field}' => [{$properties}],";
}
foreach ($table->indexes() as $index) {
+ /** @var array $fieldData */
$fieldData = $table->getIndex($index);
$properties = implode(', ', $this->_values($fieldData));
- $indexes[] = " '$index' => [$properties],";
+ $indexes[] = " '{$index}' => [{$properties}],";
}
foreach ($table->constraints() as $index) {
+ /** @var array $fieldData */
$fieldData = $table->getConstraint($index);
$properties = implode(', ', $this->_values($fieldData));
- $constraints[] = " '$index' => [$properties],";
+ $constraints[] = " '{$index}' => [{$properties}],";
}
$options = $this->_values($table->getOptions());
@@ -311,14 +318,14 @@ protected function _generateSchema(TableSchemaInterface $table): string
$content .= " '_options' => [\n" . implode(",\n", $options) . "\n ],\n";
}
- return "[\n$content ]";
+ return "[\n{$content} ]";
}
/**
* Formats Schema columns from Model Object
*
* @param array $values options keys(type, null, default, key, length, extra)
- * @return string[] Formatted values
+ * @return array Formatted values
*/
protected function _values(array $values): array
{
@@ -332,11 +339,7 @@ protected function _values(array $values): array
if ($val === 'NULL') {
$val = 'null';
}
- if (!is_numeric($key)) {
- $vals[] = "'{$key}' => {$val}";
- } else {
- $vals[] = "{$val}";
- }
+ $vals[] = is_numeric($key) ? "{$val}" : "'{$key}' => {$val}";
}
}
@@ -356,6 +359,7 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun
for ($i = 0; $i < $recordCount; $i++) {
$record = [];
foreach ($table->columns() as $field) {
+ /** @var array $fieldInfo */
$fieldInfo = $table->getColumn($field);
$insert = '';
switch ($fieldInfo['type']) {
@@ -382,7 +386,7 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun
0,
(int)$fieldInfo['length'] > 2
? (int)$fieldInfo['length'] - 2
- : (int)$fieldInfo['length']
+ : (int)$fieldInfo['length'],
);
}
}
@@ -417,6 +421,33 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun
$insert = Text::uuid();
break;
}
+ if (str_starts_with((string)$fieldInfo['type'], 'enum-')) {
+ $insert = null;
+ if ($fieldInfo['default'] || $fieldInfo['null'] === false) {
+ $dbType = TypeFactory::build($fieldInfo['type']);
+ if ($dbType instanceof EnumType) {
+ $class = $dbType->getEnumClassName();
+ $reflectionEnum = new ReflectionEnum($class);
+ $backingType = (string)$reflectionEnum->getBackingType();
+
+ if ($fieldInfo['default'] !== null) {
+ $insert = $fieldInfo['default'];
+ if ($backingType === 'int') {
+ $insert = (int)$insert;
+ }
+ } else {
+ $cases = $reflectionEnum->getCases();
+ if ($cases) {
+ $firstCase = array_shift($cases);
+ /** @var \BackedEnum $firstValue */
+ $firstValue = $firstCase->getValue();
+ $insert = $firstValue->value;
+ }
+ }
+ }
+ }
+ }
+
$record[$field] = $insert;
}
$records[] = $record;
@@ -435,9 +466,11 @@ protected function _generateRecords(TableSchemaInterface $table, int $recordCoun
protected function _makeRecordString(array $records): string
{
foreach ($records as &$record) {
- array_walk($record, function (&$value) {
- if ($value instanceof DateTimeInterface) {
+ array_walk($record, function (&$value): void {
+ if ($value instanceof DateTimeInterface || $value instanceof Chronos) {
$value = $value->format('Y-m-d H:i:s');
+ } elseif ($value instanceof ChronosDate) {
+ $value = $value->format('Y-m-d');
}
});
}
diff --git a/app/vendor/cakephp/bake/src/Command/FormCommand.php b/app/vendor/cakephp/bake/src/Command/FormCommand.php
index f36229891..95b680e8c 100644
--- a/app/vendor/cakephp/bake/src/Command/FormCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/FormCommand.php
@@ -23,10 +23,8 @@ class FormCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Form/';
+ public string $pathFragment = 'Form/';
/**
* @inheritDoc
diff --git a/app/vendor/cakephp/bake/src/Command/HelperCommand.php b/app/vendor/cakephp/bake/src/Command/HelperCommand.php
index 82205f5ab..81c43f5f4 100644
--- a/app/vendor/cakephp/bake/src/Command/HelperCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/HelperCommand.php
@@ -23,10 +23,8 @@ class HelperCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'View/Helper/';
+ public string $pathFragment = 'View/Helper/';
/**
* @inheritDoc
diff --git a/app/vendor/cakephp/bake/src/Command/MailerCommand.php b/app/vendor/cakephp/bake/src/Command/MailerCommand.php
index b96d7337e..6cd9d187e 100644
--- a/app/vendor/cakephp/bake/src/Command/MailerCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/MailerCommand.php
@@ -26,10 +26,8 @@ class MailerCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Mailer/';
+ public string $pathFragment = 'Mailer/';
/**
* @inheritDoc
@@ -63,7 +61,7 @@ public function template(): string
* @param \Cake\Console\ConsoleIo $io The console io
* @return void
*/
- public function bake(string $name, Arguments $args, ConsoleIo $io): void
+ protected function bake(string $name, Arguments $args, ConsoleIo $io): void
{
parent::bake($name, $args, $io);
}
diff --git a/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php b/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php
index 61b3a0c41..499c579b4 100644
--- a/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/MiddlewareCommand.php
@@ -23,10 +23,8 @@ class MiddlewareCommand extends SimpleBakeCommand
{
/**
* Task name used in path generation.
- *
- * @var string
*/
- public $pathFragment = 'Middleware/';
+ public string $pathFragment = 'Middleware/';
/**
* @inheritDoc
diff --git a/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php b/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php
index 8b2eaf27f..2bbae1a72 100644
--- a/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/ModelAllCommand.php
@@ -30,10 +30,7 @@ class ModelAllCommand extends BakeCommand
{
use LocatorAwareTrait;
- /**
- * @var \Bake\Command\ModelCommand
- */
- protected $modelCommand;
+ protected ModelCommand $modelCommand;
/**
* @inheritDoc
@@ -60,7 +57,7 @@ public function initialize(): void
* @param \Cake\Console\ConsoleOptionParser $parser The parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->modelCommand->buildOptionParser($parser);
$parser
@@ -83,7 +80,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
/** @var \Cake\Database\Connection $connection */
$connection = ConnectionManager::get($this->connection);
$scanner = new TableScanner($connection);
- foreach ($scanner->listUnskipped() as $table) {
+ $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped());
+ foreach ($tables as $table) {
$this->getTableLocator()->clear();
$modelArgs = new Arguments([$table], $args->getOptions(), ['name']);
$this->modelCommand->execute($modelArgs, $io);
diff --git a/app/vendor/cakephp/bake/src/Command/ModelCommand.php b/app/vendor/cakephp/bake/src/Command/ModelCommand.php
index fc9b95ec7..26e06102f 100644
--- a/app/vendor/cakephp/bake/src/Command/ModelCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/ModelCommand.php
@@ -16,7 +16,10 @@
*/
namespace Bake\Command;
+use Bake\CodeGen\ColumnTypeExtractor;
use Bake\CodeGen\FileBuilder;
+use Bake\CodeGen\ParsedFile;
+use Bake\Utility\Model\EnumParser;
use Bake\Utility\TableScanner;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
@@ -28,9 +31,13 @@
use Cake\Database\Schema\CachedCollection;
use Cake\Database\Schema\TableSchema;
use Cake\Database\Schema\TableSchemaInterface;
+use Cake\Database\Type\EnumType;
+use Cake\Database\TypeFactory;
use Cake\Datasource\ConnectionManager;
use Cake\ORM\Table;
use Cake\Utility\Inflector;
+use ReflectionEnum;
+use function Cake\Core\pluginSplit;
/**
* Command for generating model files.
@@ -39,26 +46,22 @@ class ModelCommand extends BakeCommand
{
/**
* path to Model directory
- *
- * @var string
*/
- public $pathFragment = 'Model/';
+ public string $pathFragment = 'Model/';
/**
* Table prefix
*
* Can be replaced in application subclasses if necessary
- *
- * @var string
*/
- public $tablePrefix = '';
+ public string $tablePrefix = '';
/**
* Holds tables found on connection.
*
- * @var string[]
+ * @var array
*/
- protected $_tables = [];
+ protected array $_tables = [];
/**
* Execute the command.
@@ -110,6 +113,8 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void
$tableObject = $this->getTableObject($name, $table);
$this->validateNames($tableObject->getSchema(), $io);
$data = $this->getTableContext($tableObject, $table, $name, $args, $io);
+
+ $this->bakeEnums($tableObject, $data, $args, $io);
$this->bakeTable($tableObject, $data, $args, $io);
$this->bakeEntity($tableObject, $data, $args, $io);
$this->bakeFixture($tableObject->getAlias(), $tableObject->getTable(), $args, $io);
@@ -131,7 +136,7 @@ public function validateNames(TableSchemaInterface $schema, ConsoleIo $io): void
$io->abort(sprintf(
'Unable to bake model. Table column name must start with a letter or underscore and
cannot contain special characters. Found `%s`.',
- $column
+ $column,
));
}
}
@@ -152,7 +157,7 @@ public function getTableContext(
string $table,
string $name,
Arguments $args,
- ConsoleIo $io
+ ConsoleIo $io,
): array {
$associations = $this->getAssociations($tableObject, $args, $io);
$this->applyAssociations($tableObject, $associations);
@@ -167,6 +172,7 @@ public function getTableContext(
$behaviors = $this->getBehaviors($tableObject);
$connection = $this->connection;
$hidden = $this->getHiddenFields($tableObject, $args);
+ $enumSchema = $this->getEnumDefinitions($tableObject->getSchema());
return compact(
'associations',
@@ -180,7 +186,8 @@ public function getTableContext(
'rulesChecker',
'behaviors',
'connection',
- 'hidden'
+ 'hidden',
+ 'enumSchema',
);
}
@@ -237,7 +244,7 @@ public function getAssociations(Table $table, Arguments $args, ConsoleIo $io): a
if (is_array($primary) && count($primary) > 1) {
$io->warning(
- 'Bake cannot generate associations for composite primary keys at this time.'
+ 'Bake cannot generate associations for composite primary keys at this time.',
);
return $associations;
@@ -247,7 +254,7 @@ public function getAssociations(Table $table, Arguments $args, ConsoleIo $io): a
$associations = $this->findHasMany($table, $associations);
$associations = $this->findBelongsToMany($table, $associations);
- return $associations;
+ return $this->ensureAliasUniqueness($associations);
}
/**
@@ -263,9 +270,10 @@ public function getAssociations(Table $table, Arguments $args, ConsoleIo $io): a
*/
public function applyAssociations(Table $model, array $associations): void
{
- if (get_class($model) !== Table::class) {
+ if ($model::class !== Table::class) {
return;
}
+
foreach ($associations as $type => $assocs) {
foreach ($assocs as $assoc) {
$alias = $assoc['alias'];
@@ -302,7 +310,7 @@ public function getAssociationInfo(Table $table): array
foreach ($table->associations() as $association) {
/** @var \Cake\ORM\Association $association */
- $tableClass = get_class($association->getTarget());
+ $tableClass = $association->getTarget()::class;
if ($tableClass === Table::class) {
$namespace = $appNamespace;
@@ -312,7 +320,7 @@ public function getAssociationInfo(Table $table): array
$namespace = $plugin;
}
- $namespace = str_replace('/', '\\', trim($namespace, '\\'));
+ $namespace = str_replace('/', '\\', trim((string)$namespace, '\\'));
$tableClass = $namespace . '\Model\Table\\' . $className . 'Table';
}
@@ -340,6 +348,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg
continue;
}
+ $className = null;
if ($fieldName === 'parent_id') {
$className = $this->plugin ? $this->plugin . '.' . $model->getAlias() : $model->getAlias();
$assoc = [
@@ -352,7 +361,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg
if (!$this->getTableLocator()->exists($tmpModelName)) {
$this->getTableLocator()->get(
$tmpModelName,
- ['connection' => ConnectionManager::get($this->connection)]
+ ['connection' => ConnectionManager::get($this->connection)],
);
}
$associationTable = $this->getTableLocator()->get($tmpModelName);
@@ -360,13 +369,13 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg
$tables = $this->listAll();
// Check if association model could not be instantiated as a subclass but a generic Table instance instead
if (
- get_class($associationTable) === Table::class &&
+ $associationTable::class === Table::class &&
!in_array(Inflector::tableize($tmpModelName), $tables, true)
) {
- $allowAliasRelations = $args && $args->getOption('skip-relation-check');
+ $allowAliasRelations = $args instanceof Arguments && $args->getOption('skip-relation-check');
$found = $this->findTableReferencedBy($schema, $fieldName);
if ($found) {
- $tmpModelName = Inflector::camelize($found);
+ $className = ($this->plugin ? $this->plugin . '.' : '') . Inflector::camelize($found);
} elseif (!$allowAliasRelations) {
continue;
}
@@ -375,7 +384,11 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg
'alias' => $tmpModelName,
'foreignKey' => $fieldName,
];
- if ($schema->getColumn($fieldName)['null'] === false) {
+ if ($className && $className !== $tmpModelName) {
+ $assoc['className'] = $className;
+ }
+ $columnInfo = $schema->getColumn($fieldName);
+ if ($columnInfo !== null && ($columnInfo['null'] ?? true) === false) {
$assoc['joinType'] = 'INNER';
}
}
@@ -383,6 +396,7 @@ public function findBelongsTo(Table $model, array $associations, ?Arguments $arg
if ($this->plugin && empty($assoc['className'])) {
$assoc['className'] = $this->plugin . '.' . $assoc['alias'];
}
+
$associations['belongsTo'][] = $assoc;
}
@@ -406,7 +420,7 @@ public function findTableReferencedBy(TableSchemaInterface $schema, string $keyF
foreach ($schema->constraints() as $constraint) {
$constraintInfo = $schema->getConstraint($constraint);
- if (!in_array($keyField, $constraintInfo['columns'])) {
+ if (!in_array($keyField, $constraintInfo['columns'] ?? [])) {
continue;
}
@@ -414,8 +428,8 @@ public function findTableReferencedBy(TableSchemaInterface $schema, string $keyF
continue;
}
$length = $this->tablePrefix ? mb_strlen($this->tablePrefix) : 0;
- if ($length > 0 && mb_substr($constraintInfo['references'][0], 0, $length) === $this->tablePrefix) {
- return mb_substr($constraintInfo['references'][0], $length);
+ if ($length > 0 && mb_substr((string)$constraintInfo['references'][0], 0, $length) === $this->tablePrefix) {
+ return mb_substr((string)$constraintInfo['references'][0], $length);
}
return $constraintInfo['references'][0];
@@ -466,8 +480,9 @@ public function hasUniqueConstraintFor(TableSchemaInterface $schema, string $key
foreach ($schema->constraints() as $constraint) {
$constraintInfo = $schema->getConstraint($constraint);
if (
- $constraintInfo['type'] === TableSchema::CONSTRAINT_UNIQUE &&
- $constraintInfo['columns'] === [$keyField]
+ $constraintInfo !== null &&
+ ($constraintInfo['type'] ?? null) === TableSchema::CONSTRAINT_UNIQUE &&
+ ($constraintInfo['columns'] ?? []) === [$keyField]
) {
return true;
}
@@ -624,15 +639,15 @@ public function findBelongsToMany(Table $model, array $associations): array
*
* @param \Cake\ORM\Table $model The model to introspect.
* @param \Cake\Console\Arguments $args CLI Arguments
- * @return array|string|null
+ * @return array|string
*/
- public function getDisplayField(Table $model, Arguments $args)
+ public function getDisplayField(Table $model, Arguments $args): array|string
{
if ($args->getOption('display-field')) {
return (string)$args->getOption('display-field');
}
- return $model->getDisplayField();
+ return $model->getDisplayField() ?? [];
}
/**
@@ -640,12 +655,12 @@ public function getDisplayField(Table $model, Arguments $args)
*
* @param \Cake\ORM\Table $model The model to introspect.
* @param \Cake\Console\Arguments $args CLI Arguments
- * @return string[] The columns in the primary key
+ * @return array The columns in the primary key
*/
public function getPrimaryKey(Table $model, Arguments $args): array
{
if ($args->getOption('primary-key')) {
- $fields = explode(',', $args->getOption('primary-key'));
+ $fields = explode(',', (string)$args->getOption('primary-key'));
return array_values(array_filter(array_map('trim', $fields)));
}
@@ -684,6 +699,7 @@ public function getEntityPropertySchema(Table $model): array
$schema = $model->getSchema();
foreach ($schema->columns() as $column) {
+ /** @var array $columnSchema */
$columnSchema = $schema->getColumn($column);
$properties[$column] = [
@@ -703,7 +719,7 @@ public function getEntityPropertySchema(Table $model): array
if ($plugin !== null) {
$namespace = $plugin;
}
- $namespace = str_replace('/', '\\', trim($namespace, '\\'));
+ $namespace = str_replace('/', '\\', trim((string)$namespace, '\\'));
$entityClass = $this->_entityName($association->getTarget()->getAlias());
$entityClass = '\\' . $namespace . '\Model\Entity\\' . $entityClass;
@@ -729,17 +745,17 @@ public function getEntityPropertySchema(Table $model): array
*
* @param \Cake\ORM\Table $table The table instance to get fields for.
* @param \Cake\Console\Arguments $args CLI Arguments
- * @return string[]|false|null Either an array of fields, `false` in
+ * @return array|false|null Either an array of fields, `false` in
* case the no-fields option is used, or `null` if none of the
* field options is used.
*/
- public function getFields(Table $table, Arguments $args)
+ public function getFields(Table $table, Arguments $args): array|false|null
{
if ($args->getOption('no-fields')) {
return false;
}
if ($args->getOption('fields')) {
- $fields = explode(',', $args->getOption('fields'));
+ $fields = explode(',', (string)$args->getOption('fields'));
return array_values(array_filter(array_map('trim', $fields)));
}
@@ -760,7 +776,7 @@ public function getFields(Table $table, Arguments $args)
*
* @param \Cake\ORM\Table $model The model to introspect.
* @param \Cake\Console\Arguments $args CLI Arguments
- * @return string[] The columns to make accessible
+ * @return array The columns to make accessible
*/
public function getHiddenFields(Table $model, Arguments $args): array
{
@@ -768,7 +784,7 @@ public function getHiddenFields(Table $model, Arguments $args): array
return [];
}
if ($args->getOption('hidden')) {
- $fields = explode(',', $args->getOption('hidden'));
+ $fields = explode(',', (string)$args->getOption('hidden'));
return array_values(array_filter(array_map('trim', $fields)));
}
@@ -787,7 +803,7 @@ public function getHiddenFields(Table $model, Arguments $args): array
* @param \Cake\Console\Arguments $args CLI Arguments
* @return array|false The validation rules.
*/
- public function getValidation(Table $model, array $associations, Arguments $args)
+ public function getValidation(Table $model, array $associations, Arguments $args): array|false
{
if ($args->getOption('no-validation')) {
return [];
@@ -835,7 +851,7 @@ public function fieldValidation(
TableSchemaInterface $schema,
string $fieldName,
array $metaData,
- array $primaryKey
+ array $primaryKey,
): array {
$ignoreFields = ['lft', 'rght', 'created', 'modified', 'updated'];
if (in_array($fieldName, $ignoreFields, true)) {
@@ -873,9 +889,9 @@ public function fieldValidation(
$rules['date'] = [];
} elseif ($metaData['type'] === 'time') {
$rules['time'] = [];
- } elseif (strpos($metaData['type'], 'datetime') === 0) {
+ } elseif (str_starts_with((string)$metaData['type'], 'datetime')) {
$rules['dateTime'] = [];
- } elseif (strpos($metaData['type'], 'timestamp') === 0) {
+ } elseif (str_starts_with((string)$metaData['type'], 'timestamp')) {
$rules['dateTime'] = [];
} elseif ($metaData['type'] === 'inet') {
$rules['ip'] = [];
@@ -914,8 +930,9 @@ public function fieldValidation(
}
foreach ($schema->constraints() as $constraint) {
+ /** @var array $constraint */
$constraint = $schema->getConstraint($constraint);
- if (!in_array($fieldName, $constraint['columns'], true) || count($constraint['columns']) > 1) {
+ if (!in_array($fieldName, $constraint['columns'] ?? [], true) || count($constraint['columns']) > 1) {
continue;
}
@@ -962,7 +979,7 @@ protected function getEmptyMethod(string $fieldName, array $metaData, string $pr
return $prefix . 'EmptyDateTime';
}
- if (preg_match('/file|image/', $fieldName)) {
+ if (preg_match('/(^|\s|_|-)(attachment|file|image)$/i', $fieldName)) {
return $prefix . 'EmptyFile';
}
@@ -983,31 +1000,27 @@ public function getRules(Table $model, array $associations, Arguments $args): ar
return [];
}
$schema = $model->getSchema();
- $fields = $schema->columns();
- if (empty($fields)) {
+ $schemaFields = $schema->columns();
+ if (empty($schemaFields)) {
return [];
}
- $uniqueColumns = ['username', 'login'];
- if (in_array($model->getAlias(), ['Users', 'Accounts'])) {
- $uniqueColumns[] = 'email';
- }
+ $uniqueRules = [];
+ $uniqueConstraintsColumns = [];
- $rules = [];
- foreach ($fields as $fieldName) {
- if (in_array($fieldName, $uniqueColumns, true)) {
- $rules[$fieldName] = ['name' => 'isUnique', 'fields' => [$fieldName], 'options' => []];
- }
- }
foreach ($schema->constraints() as $name) {
+ /** @var array $constraint */
$constraint = $schema->getConstraint($name);
if ($constraint['type'] !== TableSchema::CONSTRAINT_UNIQUE) {
continue;
}
$options = [];
- $fields = $constraint['columns'];
- foreach ($fields as $field) {
+ /** @var array $constraintFields */
+ $constraintFields = $constraint['columns'];
+ $uniqueConstraintsColumns = [...$uniqueConstraintsColumns, ...$constraintFields];
+
+ foreach ($constraintFields as $field) {
if ($schema->isNullable($field)) {
$allowMultiple = !ConnectionManager::get($this->connection)->getDriver() instanceof Sqlserver;
$options['allowMultipleNulls'] = $allowMultiple;
@@ -1015,15 +1028,48 @@ public function getRules(Table $model, array $associations, Arguments $args): ar
}
}
- $rules[$constraint['columns'][0]] = ['name' => 'isUnique', 'fields' => $fields, 'options' => $options];
+ $rule = ['name' => 'isUnique', 'fields' => $constraintFields, 'options' => $options];
+
+ // Add descriptive message for composite unique constraints
+ if (count($constraintFields) > 1) {
+ $rule['message'] = sprintf(
+ 'This combination of %s and %s already exists',
+ implode(', ', array_slice($constraintFields, 0, -1)),
+ end($constraintFields),
+ );
+ }
+
+ $uniqueRules[] = $rule;
+ }
+
+ $possiblyUniqueColumns = ['username', 'login'];
+ if (in_array($model->getAlias(), ['Users', 'Accounts'], true)) {
+ $possiblyUniqueColumns[] = 'email';
+ }
+
+ $possiblyUniqueRules = [];
+ foreach ($schemaFields as $field) {
+ if (
+ !in_array($field, $uniqueConstraintsColumns, true) &&
+ in_array($field, $possiblyUniqueColumns, true)
+ ) {
+ $possiblyUniqueRules[] = ['name' => 'isUnique', 'fields' => [$field], 'options' => []];
+ }
}
+ $rules = [...$possiblyUniqueRules, ...$uniqueRules];
+
if (empty($associations['belongsTo'])) {
return $rules;
}
foreach ($associations['belongsTo'] as $assoc) {
- $rules[$assoc['foreignKey']] = ['name' => 'existsIn', 'extra' => $assoc['alias'], 'options' => []];
+ $rules[] = [
+ 'name' => 'existsIn',
+ 'fields' => (array)$assoc['foreignKey'],
+ 'extra' => $assoc['alias'],
+ 'options' => [],
+ ];
}
return $rules;
@@ -1081,7 +1127,7 @@ public function getCounterCache(Table $model): array
try {
$otherSchema = $otherModel->getSchema();
- } catch (DatabaseException $e) {
+ } catch (DatabaseException) {
continue;
}
@@ -1100,7 +1146,7 @@ public function getCounterCache(Table $model): array
* Bake an entity class.
*
* @param \Cake\ORM\Table $model Model name or object
- * @param array $data An array to use to generate the Table
+ * @param array $data An array to use to generate the Table
* @param \Cake\Console\Arguments $args CLI Arguments
* @param \Cake\Console\ConsoleIo $io CLI io
* @return void
@@ -1112,7 +1158,7 @@ public function bakeEntity(Table $model, array $data, Arguments $args, ConsoleIo
}
$name = $this->_entityName($model->getAlias());
- $io->out("\n" . sprintf('Baking entity class for %s...', $name), 1, ConsoleIo::NORMAL);
+ $io->out("\n" . sprintf('Baking entity class for %s...', $name));
$namespace = Configure::read('App.namespace');
$pluginPath = '';
@@ -1152,7 +1198,7 @@ public function bakeEntity(Table $model, array $data, Arguments $args, ConsoleIo
* Bake a table class.
*
* @param \Cake\ORM\Table $model Model name or object
- * @param array $data An array to use to generate the Table
+ * @param array $data An array to use to generate the Table
* @param \Cake\Console\Arguments $args CLI Arguments
* @param \Cake\Console\ConsoleIo $io CLI Arguments
* @return void
@@ -1164,7 +1210,7 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo
}
$name = $model->getAlias();
- $io->out("\n" . sprintf('Baking table class for %s...', $name), 1, ConsoleIo::NORMAL);
+ $io->out("\n" . sprintf('Baking table class for %s...', $name));
$namespace = Configure::read('App.namespace');
$pluginPath = '';
@@ -1176,11 +1222,24 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo
$filename = $path . 'Table' . DS . $name . 'Table.php';
$parsedFile = null;
+ $customColumnTypes = [];
if ($args->getOption('update')) {
$parsedFile = $this->parseFile($filename);
+ // Extract custom column types from existing file
+ if ($parsedFile instanceof ParsedFile && isset($parsedFile->class->methods['initialize'])) {
+ $customColumnTypes = $this->extractCustomColumnTypes($parsedFile->class->methods['initialize']);
+ }
}
$entity = $this->_entityName($model->getAlias());
+ $enums = $this->enums($model, $entity, $namespace);
+
+ // Merge custom column types with generated enums
+ // Remove custom types that are now handled by enums
+ foreach ($enums as $field => $enumClass) {
+ unset($customColumnTypes[$field]);
+ }
+
$data += [
'plugin' => $this->plugin,
'pluginPath' => $pluginPath,
@@ -1194,6 +1253,8 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo
'validation' => [],
'rulesChecker' => [],
'behaviors' => [],
+ 'enums' => $enums,
+ 'customColumnTypes' => $customColumnTypes,
'connection' => $this->connection,
'fileBuilder' => new FileBuilder($io, "{$namespace}\Model\Table", $parsedFile),
];
@@ -1202,7 +1263,7 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo
->set($data)
->generate('Bake.Model/table');
- $this->writefile($io, $filename, $contents, $this->force);
+ $this->writeFile($io, $filename, $contents, $this->force);
// Work around composer caching that classes/files do not exist.
// Check for the file as it might not exist in tests.
@@ -1216,9 +1277,9 @@ public function bakeTable(Table $model, array $data, Arguments $args, ConsoleIo
}
/**
- * Outputs the a list of possible models or controllers from database
+ * Outputs the list of possible models or controllers from database
*
- * @return string[]
+ * @return array
*/
public function listAll(): array
{
@@ -1235,9 +1296,9 @@ public function listAll(): array
}
/**
- * Outputs the a list of unskipped models or controllers from database
+ * Outputs the list of unskipped models or controllers from database
*
- * @return string[]
+ * @return array
*/
public function listUnskipped(): array
{
@@ -1272,18 +1333,18 @@ public function getTable(string $name, Arguments $args): string
* @param \Cake\Console\ConsoleOptionParser $parser The parser to configure
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser->setDescription(
- 'Bake table and entity classes.'
+ 'Bake table and entity classes.',
)->addArgument('name', [
'help' => 'Name of the model to bake (without the Table suffix). ' .
'You can use Plugin.name to bake plugin models.',
])->addOption('update', [
'boolean' => true,
- 'help' => 'Update generated methods in existing files. If the file doesn\'t exist it will be created.',
+ 'help' => "Update generated methods in existing files. If the file doesn't exist it will be created.",
])->addOption('table', [
'help' => 'The table name to use if you have non-conventional table names.',
])->addOption('no-entity', [
@@ -1327,7 +1388,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'help' => 'Generate relations for all "example_id" fields'
. ' without checking the database if a table "examples" exists.',
])->setEpilog(
- 'Omitting all arguments and options will list the table names you can generate models for.'
+ 'Omitting all arguments and options will list the table names you can generate models for.',
);
return $parser;
@@ -1346,7 +1407,7 @@ public function bakeFixture(
string $className,
string $useTable,
Arguments $args,
- ConsoleIo $io
+ ConsoleIo $io,
): void {
if ($args->getOption('no-fixture')) {
return;
@@ -1355,7 +1416,7 @@ public function bakeFixture(
$fixtureArgs = new Arguments(
[$className],
['table' => $useTable] + $args->getOptions(),
- ['name']
+ ['name'],
);
$fixture->execute($fixtureArgs, $io);
}
@@ -1377,8 +1438,192 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi
$testArgs = new Arguments(
['table', $className],
$args->getOptions(),
- ['type', 'name']
+ ['type', 'name'],
);
$test->execute($testArgs, $io);
}
+
+ /**
+ * @return array
+ */
+ protected function enums(Table $table, string $entity, string $namespace): array
+ {
+ $fields = $this->possibleEnumFields($table->getSchema());
+ $enumClassNamespace = $namespace . '\Model\Enum\\';
+
+ $enums = [];
+ foreach ($fields as $field) {
+ $enumClassName = $enumClassNamespace . $entity . Inflector::camelize($field);
+ if (!class_exists($enumClassName)) {
+ continue;
+ }
+
+ $enums[$field] = $enumClassName;
+ }
+
+ return $enums;
+ }
+
+ /**
+ * @return array
+ */
+ protected function possibleEnumFields(TableSchemaInterface $schema): array
+ {
+ $fields = [];
+
+ foreach ($schema->columns() as $column) {
+ /** @var array $columnSchema */
+ $columnSchema = $schema->getColumn($column);
+ if (str_starts_with((string)$columnSchema['type'], 'enum-')) {
+ $fields[] = $column;
+
+ continue;
+ }
+
+ if (!in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)) {
+ continue;
+ }
+
+ $fields[] = $column;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getEnumDefinitions(TableSchemaInterface $schema): array
+ {
+ $enums = [];
+
+ foreach ($schema->columns() as $column) {
+ /** @var array $columnSchema */
+ $columnSchema = $schema->getColumn($column);
+ if (
+ !in_array($columnSchema['type'], ['string', 'integer', 'tinyinteger', 'smallinteger'], true)
+ && !str_starts_with((string)$columnSchema['type'], 'enum-')
+ ) {
+ continue;
+ }
+
+ if (empty($columnSchema['comment']) || !str_contains((string)$columnSchema['comment'], '[enum]')) {
+ continue;
+ }
+
+ $enumsDefinitionString = EnumParser::parseDefinitionString($columnSchema['comment']);
+ $isInt = in_array($columnSchema['type'], ['integer', 'tinyinteger', 'smallinteger'], true);
+ if (str_starts_with((string)$columnSchema['type'], 'enum-')) {
+ $dbType = TypeFactory::build($columnSchema['type']);
+ if ($dbType instanceof EnumType) {
+ $class = $dbType->getEnumClassName();
+ $reflectionEnum = new ReflectionEnum($class);
+ $backingType = (string)$reflectionEnum->getBackingType();
+ if ($backingType === 'int') {
+ $isInt = true;
+ }
+ }
+ }
+ $enumsDefinition = EnumParser::parseCases($enumsDefinitionString, $isInt);
+ if (!$enumsDefinition) {
+ continue;
+ }
+
+ $enums[$column] = [
+ 'type' => $isInt ? 'int' : 'string',
+ 'cases' => $enumsDefinition,
+ ];
+ }
+
+ return $enums;
+ }
+
+ /**
+ * @param array $data
+ * @return void
+ */
+ protected function bakeEnums(Table $model, array $data, Arguments $args, ConsoleIo $io): void
+ {
+ $enums = $data['enumSchema'];
+ if (!$enums) {
+ return;
+ }
+
+ $entity = $this->_entityName($model->getAlias());
+
+ foreach ($enums as $column => $data) {
+ $enumCommand = new EnumCommand();
+
+ $name = $entity . Inflector::camelize($column);
+ if ($this->plugin) {
+ $name = $this->plugin . '.' . $name;
+ }
+
+ $enumCases = $data['cases'];
+
+ $cases = [];
+ foreach ($enumCases as $k => $v) {
+ $cases[] = $k . ':' . $v;
+ }
+
+ $args = new Arguments(
+ [$name, implode(',', $cases)],
+ ['int' => $data['type'] === 'int'] + $args->getOptions(),
+ ['name', 'cases'],
+ );
+ $enumCommand->execute($args, $io);
+ }
+ }
+
+ /**
+ * @param array> $associations
+ * @return array>
+ */
+ protected function ensureAliasUniqueness(array $associations): array
+ {
+ $existing = [];
+ foreach ($associations as $type => $associationsPerType) {
+ foreach ($associationsPerType as $k => $association) {
+ $alias = $association['alias'];
+ if (in_array($alias, $existing, true)) {
+ $alias = $this->createAssociationAlias($association);
+ }
+ $existing[] = $alias;
+ if (empty($association['className'])) {
+ $className = $this->plugin ? $this->plugin . '.' . $association['alias'] : $association['alias'];
+ if ($className !== $alias) {
+ $association['className'] = $className;
+ }
+ }
+ $association['alias'] = $alias;
+ $associations[$type][$k] = $association;
+ }
+ }
+
+ return $associations;
+ }
+
+ /**
+ * @param array $association
+ * @return string
+ */
+ protected function createAssociationAlias(array $association): string
+ {
+ $foreignKey = $association['foreignKey'];
+
+ return $this->_modelNameFromKey($foreignKey);
+ }
+
+ /**
+ * Extract custom column type mappings from existing initialize method
+ *
+ * @param string $initializeMethod The initialize method code
+ * @return array Map of column names to type expressions
+ */
+ protected function extractCustomColumnTypes(string $initializeMethod): array
+ {
+ $extractor = new ColumnTypeExtractor();
+
+ return $extractor->extract($initializeMethod);
+ }
}
diff --git a/app/vendor/cakephp/bake/src/Command/PluginCommand.php b/app/vendor/cakephp/bake/src/Command/PluginCommand.php
index 992ba2ee9..73665d06e 100644
--- a/app/vendor/cakephp/bake/src/Command/PluginCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/PluginCommand.php
@@ -26,8 +26,10 @@
use Cake\Core\App;
use Cake\Core\Configure;
use Cake\Core\Plugin;
-use Cake\Filesystem\Filesystem;
+use Cake\Utility\Filesystem;
use Cake\Utility\Inflector;
+use RuntimeException;
+use function Cake\Core\env;
/**
* The Plugin Command handles creating an empty plugin, ready to be used
@@ -36,21 +38,10 @@ class PluginCommand extends BakeCommand
{
/**
* Plugin path.
- *
- * @var string
*/
- public $path;
+ public string $path;
- /**
- * initialize
- *
- * @return void
- */
- public function initialize(): void
- {
- parent::initialize();
- $this->path = current(App::path('plugins'));
- }
+ protected bool $isVendor = false;
/**
* Execute the command.
@@ -63,16 +54,28 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
{
$name = $args->getArgument('name');
if (empty($name)) {
- $io->err('You must provide a plugin name in CamelCase format. ');
- $io->err('To make an "MyExample" plugin, run `cake bake plugin MyExample` .');
+ $io->error('You must provide a plugin name in CamelCase format.');
+ $io->out('To make an "MyExample" plugin, run `cake bake plugin MyExample` .');
return static::CODE_ERROR;
}
$parts = explode('/', $name);
- $plugin = implode('/', array_map([Inflector::class, 'camelize'], $parts));
+ $plugin = implode('/', array_map(Inflector::camelize(...), $parts));
+
+ if ($args->getOption('standalone-path')) {
+ $this->path = (string)$args->getOption('standalone-path');
+ $this->path = rtrim($this->path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ $this->isVendor = true;
+
+ if (!is_dir($this->path)) {
+ $io->error(sprintf('Path `%s` does not exist.', $this->path));
+
+ return static::CODE_ERROR;
+ }
+ }
$pluginPath = $this->_pluginPath($plugin);
- if (is_dir($pluginPath)) {
+ if (is_dir($pluginPath) && !$args->getOption('class-only')) {
$io->out(sprintf('Plugin: %s already exists, no action taken', $plugin));
$io->out(sprintf('Path: %s', $pluginPath));
@@ -98,10 +101,16 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
*/
public function bake(string $plugin, Arguments $args, ConsoleIo $io): ?bool
{
- $pathOptions = App::path('plugins');
- if (count($pathOptions) > 1) {
- $this->findPath($pathOptions, $io);
+ if (!$this->isVendor) {
+ $pathOptions = App::path('plugins');
+ $currentPath = current($pathOptions);
+ $this->path = $currentPath !== false ? $currentPath : '';
+
+ if (count($pathOptions) > 1) {
+ $this->findPath($pathOptions, $io);
+ }
}
+
$io->out(sprintf('Plugin Name: %s', $plugin));
$io->out(sprintf('Plugin Directory: %s', $this->path . $plugin));
$io->hr();
@@ -114,8 +123,34 @@ public function bake(string $plugin, Arguments $args, ConsoleIo $io): ?bool
$this->_generateFiles($plugin, $this->path, $args, $io);
- $this->_modifyAutoloader($plugin, $this->path, $args, $io);
- $this->_modifyApplication($plugin, $io);
+ if (!$this->isVendor) {
+ if (!$args->getOption('class-only')) {
+ $this->_modifyApplication($plugin, $io);
+ }
+
+ $composer = $this->findComposer($args, $io);
+ if ($composer === false) {
+ $io->error('Could not find composer executable.');
+ $this->abort();
+ }
+
+ try {
+ /** @var non-empty-string $cwd */
+ $cwd = getcwd();
+
+ // Windows makes running multiple commands at once hard.
+ chdir(dirname($this->_rootComposerFilePath()));
+ $command = 'php ' . escapeshellarg($composer) . ' dump-autoload';
+ $process = new Process($io);
+ $io->out($process->call($command));
+
+ chdir($cwd);
+ } catch (RuntimeException $e) {
+ $error = $e->getMessage();
+ $io->error(sprintf('Could not run `composer dump-autoload`: %s', $error));
+ $this->abort();
+ }
+ }
$io->hr();
$io->out(sprintf('Created: %s in %s', $plugin, $this->path . $plugin), 2);
@@ -156,22 +191,21 @@ protected function _generateFiles(
string $pluginName,
string $path,
Arguments $args,
- ConsoleIo $io
+ ConsoleIo $io,
): void {
$namespace = str_replace('/', '\\', $pluginName);
$baseNamespace = Configure::read('App.namespace');
$name = $pluginName;
$vendor = 'your-name-here';
- if (strpos($pluginName, '/') !== false) {
+ if (str_contains($pluginName, '/')) {
[$vendor, $name] = explode('/', $pluginName);
}
$package = Inflector::dasherize($vendor) . '/' . Inflector::dasherize($name);
- /** @psalm-suppress UndefinedConstant */
$composerConfig = json_decode(
- file_get_contents(ROOT . DS . 'composer.json'),
- true
+ (string)file_get_contents(ROOT . DS . 'composer.json'),
+ true,
);
$renderer = $this->createTemplateRenderer()
@@ -191,7 +225,7 @@ protected function _generateFiles(
$paths = [];
if ($args->hasOption('theme')) {
- $paths[] = Plugin::templatePath($args->getOption('theme'));
+ $paths[] = Plugin::templatePath((string)$args->getOption('theme'));
}
$paths = array_merge($paths, Configure::read('App.paths.templates'));
@@ -202,19 +236,40 @@ protected function _generateFiles(
do {
$templatesPath = array_shift($paths) . BakeView::BAKE_TEMPLATE_FOLDER . '/Plugin';
if (is_dir($templatesPath)) {
- $templates = array_keys(iterator_to_array(
- $fs->findRecursive($templatesPath, '/\.twig$/')
- ));
+ $files = iterator_to_array(
+ $fs->findRecursive($templatesPath, '/\.twig$/'),
+ );
+
+ if (!$this->isVendor) {
+ $vendorFiles = [
+ '.gitignore.twig', 'README.md.twig', 'composer.json.twig', 'phpunit.xml.dist.twig',
+ 'bootstrap.php.twig', 'schema.sql.twig',
+ ];
+
+ foreach ($files as $key => $file) {
+ if (in_array($file->getFilename(), $vendorFiles, true)) {
+ unset($files[$key]);
+ }
+ }
+ }
+
+ if ($args->getOption('class-only')) {
+ $files = array_filter($files, function ($file): bool {
+ return $file->getFilename() === 'Plugin.php.twig';
+ });
+ }
+
+ $templates = array_keys($files);
}
} while (!$templates);
sort($templates);
foreach ($templates as $template) {
- $template = substr($template, strrpos($template, 'Plugin' . DIRECTORY_SEPARATOR) + 7, -4);
+ $template = substr((string)$template, strrpos((string)$template, 'Plugin' . DIRECTORY_SEPARATOR) + 7, -4);
$template = rtrim($template, '.');
$filename = $template;
- if ($filename === 'src/Plugin.php') {
- $filename = 'src/' . $name . 'Plugin.php';
+ if ($filename === 'src' . DIRECTORY_SEPARATOR . 'Plugin.php') {
+ $filename = 'src' . DIRECTORY_SEPARATOR . $name . 'Plugin.php';
}
$this->_generateFile($renderer, $template, $root, $filename, $io);
}
@@ -235,76 +290,13 @@ protected function _generateFile(
string $template,
string $root,
string $filename,
- ConsoleIo $io
+ ConsoleIo $io,
): void {
$io->out(sprintf('Generating %s file...', $template));
$out = $renderer->generate('Bake.Plugin/' . $template);
$io->createFile($root . $filename, $out);
}
- /**
- * Modifies App's composer.json to include the plugin and tries to call
- * composer dump-autoload to refresh the autoloader cache
- *
- * @param string $plugin Name of plugin
- * @param string $path The path to save the phpunit.xml file to.
- * @param \Cake\Console\Arguments $args The Arguments instance.
- * @param \Cake\Console\ConsoleIo $io The io instance.
- * @return bool True if composer could be modified correctly
- */
- protected function _modifyAutoloader(
- string $plugin,
- string $path,
- Arguments $args,
- ConsoleIo $io
- ): bool {
- $file = $this->_rootComposerFilePath();
-
- if (!file_exists($file)) {
- $io->out(sprintf('Main composer file %s not found ', $file));
-
- return false;
- }
-
- $autoloadPath = str_replace(ROOT . DS, '', $this->path);
- $autoloadPath = str_replace('\\', '/', $autoloadPath);
- $namespace = str_replace('/', '\\', $plugin);
-
- $config = json_decode(file_get_contents($file), true);
- $config['autoload']['psr-4'][$namespace . '\\'] = $autoloadPath . $plugin . '/src/';
- $config['autoload-dev']['psr-4'][$namespace . '\\Test\\'] = $autoloadPath . $plugin . '/tests/';
-
- $io->out('Modifying composer autoloader ');
-
- $out = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
- $io->createFile($file, $out, $this->force);
-
- $composer = $this->findComposer($args, $io);
-
- if (!$composer) {
- $io->error('Could not locate composer. Add composer to your PATH, or use the --composer option.');
- $this->abort();
- }
-
- try {
- $cwd = getcwd();
-
- // Windows makes running multiple commands at once hard.
- chdir(dirname($this->_rootComposerFilePath()));
- $command = 'php ' . escapeshellarg($composer) . ' dump-autoload';
- $process = new Process($io);
- $io->out($process->call($command));
-
- chdir($cwd);
- } catch (\RuntimeException $e) {
- $error = $e->getMessage();
- $io->error(sprintf('Could not run `composer dump-autoload`: %s', $error));
- $this->abort();
- }
-
- return true;
- }
-
/**
* The path to the main application's composer file
*
@@ -366,13 +358,14 @@ public function findPath(array $pathOptions, ConsoleIo $io): void
* @param \Cake\Console\ConsoleOptionParser $parser The option parser
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription(
'Create the directory structure, AppController class and testing setup for a new plugin. ' .
- 'Can create plugins in any of your bootstrapped plugin paths.'
+ 'Can create plugins in any of your bootstrapped plugin paths.',
)->addArgument('name', [
- 'help' => 'CamelCased name of the plugin to create.',
+ 'help' => 'CamelCased name of the plugin to create.'
+ . ' For standalone plugins you can use vendor prefixed names like MyVendor/MyPlugin.',
])->addOption('composer', [
'default' => ROOT . DS . 'composer.phar',
'help' => 'The path to the composer executable.',
@@ -385,6 +378,14 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'help' => 'The theme to use when baking code.',
'default' => Configure::read('Bake.theme') ?: null,
'choices' => $this->_getBakeThemes(),
+ ])
+ ->addOption('standalone-path', [
+ 'short' => 'p',
+ 'help' => 'Generate a standalone plugin in the provided path.',
+ ])->addOption('class-only', [
+ 'short' => 'c',
+ 'boolean' => true,
+ 'help' => 'Generate only the plugin class.',
]);
return $parser;
@@ -395,9 +396,9 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
*
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
- * @return string|bool Either the path to composer or false if it cannot be found.
+ * @return string|false Either the path to composer or false if it cannot be found.
*/
- public function findComposer(Arguments $args, ConsoleIo $io)
+ public function findComposer(Arguments $args, ConsoleIo $io): string|bool
{
if ($args->hasOption('composer')) {
/** @var string $path */
@@ -407,7 +408,7 @@ public function findComposer(Arguments $args, ConsoleIo $io)
}
}
$composer = false;
- $path = env('PATH');
+ $path = (string)env('PATH');
if (!empty($path)) {
$paths = explode(PATH_SEPARATOR, $path);
$composer = $this->_searchPath($paths, $io);
@@ -421,9 +422,9 @@ public function findComposer(Arguments $args, ConsoleIo $io)
*
* @param array $path The paths to search.
* @param \Cake\Console\ConsoleIo $io The console io
- * @return string|bool
+ * @return string|false
*/
- protected function _searchPath(array $path, ConsoleIo $io)
+ protected function _searchPath(array $path, ConsoleIo $io): string|bool
{
$composer = ['composer.phar', 'composer'];
foreach ($path as $dir) {
diff --git a/app/vendor/cakephp/bake/src/Command/ShellHelperCommand.php b/app/vendor/cakephp/bake/src/Command/ShellHelperCommand.php
deleted file mode 100644
index 55643663d..000000000
--- a/app/vendor/cakephp/bake/src/Command/ShellHelperCommand.php
+++ /dev/null
@@ -1,54 +0,0 @@
-extractCommonProperties($args);
$name = $args->getArgumentAt(0);
if (empty($name)) {
- $io->err('You must provide a name to bake a ' . $this->name());
+ $io->error('You must provide a name to bake a ' . $this->name());
$this->abort();
}
$name = $this->_getName($name);
@@ -135,17 +135,17 @@ public function bakeTest(string $className, Arguments $args, ConsoleIo $io): voi
* @param \Cake\Console\ConsoleOptionParser $parser Option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$name = $this->name();
$parser->setDescription(
- sprintf('Bake a %s class file.', $name)
+ sprintf('Bake a %s class file.', $name),
)->addArgument('name', [
'help' => sprintf(
'Name of the %s to bake. Can use Plugin.name to bake %s files into plugins.',
$name,
- $name
+ $name,
),
])->addOption('no-test', [
'boolean' => true,
diff --git a/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php b/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php
index dd16f13ad..0a1c72e5e 100644
--- a/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/TemplateAllCommand.php
@@ -27,10 +27,7 @@
*/
class TemplateAllCommand extends BakeCommand
{
- /**
- * @var \Bake\Command\TemplateCommand
- */
- protected $templateCommand;
+ protected TemplateCommand $templateCommand;
/**
* @inheritDoc
@@ -65,8 +62,15 @@ public function execute(Arguments $args, ConsoleIo $io): int
$connection = ConnectionManager::get($this->connection);
$scanner = new TableScanner($connection);
- foreach ($scanner->listUnskipped() as $table) {
- $templateArgs = new Arguments([$table], $args->getOptions(), ['name']);
+ $tables = $scanner->removeShadowTranslationTables($scanner->listUnskipped());
+ foreach ($tables as $table) {
+ $parser = $this->templateCommand->getOptionParser();
+ $templateArgs = new Arguments(
+ [$table],
+ $args->getOptions(),
+ $parser->argumentNames(),
+ );
+
$this->templateCommand->execute($templateArgs, $io);
}
@@ -79,7 +83,7 @@ public function execute(Arguments $args, ConsoleIo $io): int
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser
@@ -88,7 +92,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'help' => 'The routing prefix to generate views for.',
])->addOption('index-columns', [
'help' => 'Limit for the number of index columns',
- 'default' => 0,
+ 'default' => '0',
]);
return $parser;
diff --git a/app/vendor/cakephp/bake/src/Command/TemplateCommand.php b/app/vendor/cakephp/bake/src/Command/TemplateCommand.php
index 57d9372da..b41707df3 100644
--- a/app/vendor/cakephp/bake/src/Command/TemplateCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/TemplateCommand.php
@@ -28,7 +28,9 @@
use Cake\ORM\Table;
use Cake\Utility\Inflector;
use Cake\View\Exception\MissingTemplateException;
+use Exception;
use RuntimeException;
+use function Cake\Core\namespaceSplit;
/**
* Task class for creating view template files.
@@ -37,59 +39,47 @@ class TemplateCommand extends BakeCommand
{
/**
* Name of the controller being used
- *
- * @var string
*/
- public $controllerName;
+ public string $controllerName;
/**
* Classname of the controller being used
- *
- * @var string
*/
- public $controllerClass;
+ public string $controllerClass;
/**
* Name with plugin of the model being used
- *
- * @var string
*/
- public $modelName = null;
+ public string $modelName;
/**
* Actions to use for scaffolding
*
- * @var string[]
+ * @var array
*/
- public $scaffoldActions = ['index', 'view', 'add', 'edit'];
+ public array $scaffoldActions = ['index', 'view', 'add', 'edit'];
/**
* Actions that exclude hidden fields
*
- * @var string[]
+ * @var array
*/
- public $excludeHiddenActions = ['index', 'view'];
+ public array $excludeHiddenActions = ['index', 'view'];
/**
* AssociationFilter utility
- *
- * @var \Bake\Utility\Model\AssociationFilter|null
*/
- protected $_associationFilter;
+ protected ?AssociationFilter $_associationFilter = null;
/**
* Template path.
- *
- * @var string
*/
- public $path;
+ public string $path;
/**
* Output extension
- *
- * @var string
*/
- public $ext = 'php';
+ public string $ext = 'php';
/**
* Override initialize
@@ -98,7 +88,17 @@ class TemplateCommand extends BakeCommand
*/
public function initialize(): void
{
- $this->path = current(App::path('templates'));
+ parent::initialize();
+
+ $templatePaths = App::path('templates');
+ if ($templatePaths === []) {
+ throw new RuntimeException(
+ 'Could not read template paths. ' .
+ 'Ensure `App.paths.templates` is defined in your application configuration.',
+ );
+ }
+
+ $this->path = current($templatePaths);
}
/**
@@ -128,8 +128,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$template = $args->getArgument('template');
$action = $args->getArgument('action');
- $controller = $args->getOption('controller');
- $this->controller($args, $name, $controller);
+ $this->controller($args, $name, (string)$args->getOption('controller'));
$this->model($name);
if ($template && $action === null) {
@@ -169,7 +168,7 @@ public function model(string $table): void
$tableName = $this->_camelize($table);
$plugin = $this->plugin;
if ($plugin) {
- $plugin = $plugin . '.';
+ $plugin .= '.';
}
$this->modelName = $plugin . $tableName;
}
@@ -211,15 +210,14 @@ public function controller(Arguments $args, string $table, ?string $controller =
public function getTemplatePath(Arguments $args, ?string $container = null): string
{
$path = parent::getTemplatePath($args, $container);
- $path .= $this->controllerName . DS;
- return $path;
+ return $path . $this->controllerName . DS;
}
/**
* Get a list of actions that can / should have view templates baked for them.
*
- * @return string[] Array of action names that should be baked
+ * @return array Array of action names that should be baked
*/
protected function _methodsToBake(): array
{
@@ -230,12 +228,12 @@ protected function _methodsToBake(): array
$methods = array_diff(
array_map(
'Cake\Utility\Inflector::underscore',
- get_class_methods($this->controllerClass)
+ get_class_methods($this->controllerClass),
),
array_map(
'Cake\Utility\Inflector::underscore',
- get_class_methods($base . '\Controller\AppController')
- )
+ get_class_methods($base . '\Controller\AppController'),
+ ),
);
}
if (empty($methods)) {
@@ -293,7 +291,7 @@ protected function _loadController(ConsoleIo $io): array
$fields = $schema->columns();
$hidden = $modelObject->newEmptyEntity()->getHidden() ?: ['token', 'password', 'passwd'];
$modelClass = $this->modelName;
- } catch (\Exception $exception) {
+ } catch (Exception $exception) {
$io->error($exception->getMessage());
$this->abort();
}
@@ -314,6 +312,12 @@ protected function _loadController(ConsoleIo $io): array
$pluralVar = Inflector::variable($this->controllerName);
$pluralHumanName = $this->_pluralHumanName($this->controllerName);
+ // Handle cases where singular and plural are identical (e.g., "news", "sheep")
+ // to avoid generating invalid code like `foreach ($news as $news)`
+ if ($singularVar === $pluralVar) {
+ $singularVar .= 'Entity';
+ }
+
return compact(
'modelObject',
'modelClass',
@@ -329,7 +333,7 @@ protected function _loadController(ConsoleIo $io): array
'hidden',
'associations',
'keyFields',
- 'namespace'
+ 'namespace',
);
}
@@ -347,8 +351,8 @@ public function bake(
Arguments $args,
ConsoleIo $io,
string $template,
- $content = '',
- ?string $outputFile = null
+ string|bool $content = '',
+ ?string $outputFile = null,
): void {
if ($outputFile === null) {
$outputFile = $template;
@@ -358,14 +362,14 @@ public function bake(
}
if (empty($content)) {
// phpcs:ignore Generic.Files.LineLength
- $io->err("No generated content for '{$template}.{$this->ext}', not generating template. ");
+ $io->warning("No generated content for '{$template}.{$this->ext}', not generating template.");
return;
}
$path = $this->getTemplatePath($args);
$filename = $path . Inflector::underscore($outputFile) . '.' . $this->ext;
- $io->out("\n" . sprintf('Baking `%s` view template file...', $outputFile), 1, ConsoleIo::NORMAL);
+ $io->out("\n" . sprintf('Baking `%s` view template file...', $outputFile));
$io->createFile($filename, $content, $this->force);
}
@@ -404,7 +408,7 @@ public function getContent(Arguments $args, ConsoleIo $io, string $action, ?arra
}
$renderer->set('indexColumns', $indexColumns);
- return $renderer->generate("Bake.Template/$action");
+ return $renderer->generate("Bake.Template/{$action}");
}
/**
@@ -413,12 +417,12 @@ public function getContent(Arguments $args, ConsoleIo $io, string $action, ?arra
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to update.
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$parser->setDescription(
- 'Bake views for a controller, using built-in or custom templates. '
+ 'Bake views for a controller, using built-in or custom templates. ',
)->addArgument('name', [
'help' => 'Name of the controller views to bake. You can use Plugin.name as a shortcut for plugin baking.',
])->addArgument('template', [
@@ -431,7 +435,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'help' => 'The routing prefix to generate views for.',
])->addOption('index-columns', [
'help' => 'Limit for the number of index columns',
- 'default' => 0,
+ 'default' => '0',
]);
return $parser;
@@ -446,7 +450,7 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
*/
protected function _filteredAssociations(Table $model): array
{
- if ($this->_associationFilter === null) {
+ if (!$this->_associationFilter instanceof AssociationFilter) {
$this->_associationFilter = new AssociationFilter();
}
diff --git a/app/vendor/cakephp/bake/src/Command/TestCommand.php b/app/vendor/cakephp/bake/src/Command/TestCommand.php
index 9fe9cd91f..c62c4c191 100644
--- a/app/vendor/cakephp/bake/src/Command/TestCommand.php
+++ b/app/vendor/cakephp/bake/src/Command/TestCommand.php
@@ -19,18 +19,18 @@
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
-use Cake\Console\Shell;
use Cake\Controller\Controller;
use Cake\Core\Configure;
use Cake\Core\Exception\CakeException;
use Cake\Core\Plugin;
-use Cake\Filesystem\Filesystem;
-use Cake\Http\Response;
use Cake\Http\ServerRequest as Request;
use Cake\ORM\Table;
+use Cake\Utility\Filesystem;
use Cake\Utility\Inflector;
use ReflectionClass;
use UnexpectedValueException;
+use function Cake\Core\namespaceSplit;
+use function Cake\Core\pluginSplit;
/**
* Command class for generating test files.
@@ -40,64 +40,60 @@ class TestCommand extends BakeCommand
/**
* class types that methods can be generated for
*
- * @var string[]
+ * @var array
*/
- public $classTypes = [
+ public array $classTypes = [
'Entity' => 'Model\Entity',
'Table' => 'Model\Table',
'Controller' => 'Controller',
'Component' => 'Controller\Component',
'Behavior' => 'Model\Behavior',
'Helper' => 'View\Helper',
- 'Shell' => 'Shell',
- 'Task' => 'Shell\Task',
- 'ShellHelper' => 'Shell\Helper',
'Cell' => 'View\Cell',
'Form' => 'Form',
'Mailer' => 'Mailer',
'Command' => 'Command',
'CommandHelper' => 'Command\Helper',
'Middleware' => 'Middleware',
+ 'Class' => '',
];
/**
* class types that methods can be generated for
*
- * @var string[]
+ * @var array
*/
- public $classSuffixes = [
+ public array $classSuffixes = [
'Entity' => '',
'Table' => 'Table',
'Controller' => 'Controller',
'Component' => 'Component',
'Behavior' => 'Behavior',
'Helper' => 'Helper',
- 'Shell' => 'Shell',
- 'Task' => 'Task',
- 'ShellHelper' => 'Helper',
'Cell' => 'Cell',
'Form' => 'Form',
'Mailer' => 'Mailer',
'Command' => 'Command',
'CommandHelper' => 'Helper',
'Middleware' => 'Middleware',
+ 'Class' => '',
];
/**
* Blacklisted methods for controller test cases.
*
- * @var string[]
+ * @var array
*/
- protected $blacklistedMethods = [
+ protected array $blacklistedMethods = [
'initialize',
];
/**
* Internal list of fixtures that have been added so far.
*
- * @var string[]
+ * @var array
*/
- protected $_fixtures = [];
+ protected array $_fixtures = [];
/**
* Execute test generation
@@ -114,7 +110,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
return null;
}
- $type = $this->normalize($args->getArgument('type'));
+ $type = $this->normalize((string)$args->getArgument('type'));
if ($args->getOption('all')) {
$this->_bakeAll($type, $args, $io);
@@ -126,11 +122,15 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
return null;
}
- $name = $args->getArgument('name');
+ $name = (string)$args->getArgument('name');
$name = $this->_getName($name);
- if ($this->bake($type, $name, $args, $io)) {
- $io->out('Done ');
+ $result = $this->bake($type, $name, $args, $io);
+ if ($result === static::CODE_ERROR) {
+ return static::CODE_ERROR;
+ }
+ if ($result) {
+ $io->success('Done');
}
return static::CODE_SUCCESS;
@@ -146,10 +146,10 @@ protected function outputTypeChoices(ConsoleIo $io): void
{
$io->out(
'You must provide a class type to bake a test for. The valid types are:',
- 2
+ 2,
);
$i = 0;
- foreach ($this->classTypes as $option => $package) {
+ foreach (array_keys($this->classTypes) as $option) {
$io->out(++$i . '. ' . $option);
}
$io->out('');
@@ -168,7 +168,7 @@ protected function outputClassChoices(string $typeName, ConsoleIo $io): void
$type = $this->mapType($typeName);
$io->out(
'You must provide a class to bake a test for. Some possible options are:',
- 2
+ 2,
);
$options = $this->_getClassOptions($type);
$i = 0;
@@ -194,20 +194,20 @@ protected function _bakeAll(string $type, Arguments $args, ConsoleIo $io): void
foreach ($classes as $class) {
if ($this->bake($type, $class, $args, $io)) {
- $io->out('Done - ' . $class . ' ');
+ $io->success('Done - ' . $class);
} else {
- $io->out('Failed - ' . $class . ' ');
+ $io->error('Failed - ' . $class);
}
}
- $io->out('Bake finished ');
+ $io->info('Bake finished');
}
/**
* Get the possible classes for a given type.
*
* @param string $namespace The namespace fragment to look for classes in.
- * @return string[]
+ * @return array
*/
protected function _getClassOptions(string $namespace): array
{
@@ -218,10 +218,26 @@ protected function _getClassOptions(string $namespace): array
}
$path = $base . str_replace('\\', DS, $namespace);
- $files = (new Filesystem())->find($path);
- foreach ($files as $fileObj) {
- if ($fileObj->isFile()) {
- $classes[] = substr($fileObj->getFileName(), 0, -4) ?: '';
+
+ // For generic Class type (empty namespace), search recursively
+ if ($namespace === '') {
+ $files = (new Filesystem())->findRecursive($path, '/\.php$/');
+ foreach ($files as $fileObj) {
+ if ($fileObj->isFile() && $fileObj->getFileName() !== 'Application.php') {
+ // Build the namespace path relative to App directory
+ /** @var string $relativePath */
+ $relativePath = str_replace($base, '', $fileObj->getPath());
+ $relativePath = trim(str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath), '\\');
+ $className = substr((string)$fileObj->getFileName(), 0, -4) ?: '';
+ $classes[] = $relativePath ? $relativePath . '\\' . $className : $className;
+ }
+ }
+ } else {
+ $files = (new Filesystem())->find($path);
+ foreach ($files as $fileObj) {
+ if ($fileObj->isFile()) {
+ $classes[] = substr((string)$fileObj->getFileName(), 0, -4) ?: '';
+ }
}
}
sort($classes);
@@ -236,26 +252,58 @@ protected function _getClassOptions(string $namespace): array
* @param string $className the 'cake name' for the class ie. Posts for the PostsController
* @param \Cake\Console\Arguments $args Arguments
* @param \Cake\Console\ConsoleIo $io ConsoleIo instance
- * @return string|bool
+ * @return string|bool|int Returns the generated code as string on success, false on failure, or CODE_ERROR for validation errors
*/
- public function bake(string $type, string $className, Arguments $args, ConsoleIo $io)
+ public function bake(string $type, string $className, Arguments $args, ConsoleIo $io): string|bool|int
{
$type = $this->normalize($type);
if (!isset($this->classSuffixes[$type]) || !isset($this->classTypes[$type])) {
return false;
}
+ // For Class type, validate that backslashes are properly escaped
+ if ($type === 'Class' && !str_contains($className, '\\')) {
+ $io->error('Class name appears to have no namespace separators.');
+ $io->out('');
+ $io->out('If you meant to specify a namespaced class, please use quotes:');
+ $io->out(" bin/cake bake test class '{$className}' ");
+ $io->out('');
+ $io->out('Or specify without the base namespace:');
+ $io->out(' bin/cake bake test class YourNamespace\ClassName ');
+
+ return static::CODE_ERROR;
+ }
+
$prefix = $this->getPrefix($args);
$fullClassName = $this->getRealClassName($type, $className, $prefix);
+ // For Class type, validate that the class exists
+ if ($type === 'Class' && !class_exists($fullClassName)) {
+ $io->error("Class '{$fullClassName}' does not exist or cannot be loaded.");
+ $io->out('');
+ $io->out('Please check:');
+ $io->out(' - The class file exists in the correct location');
+ $io->out(' - The class is properly autoloaded');
+ $io->out(' - The namespace and class name are correct');
+
+ return static::CODE_ERROR;
+ }
+
+ // Check if fixture factories plugin is available
+ $hasFixtureFactories = $this->hasFixtureFactories();
+
if (!$args->getOption('no-fixture')) {
- if ($args->getOption('fixtures')) {
- $fixtures = array_map('trim', explode(',', $args->getOption('fixtures')));
+ if ($hasFixtureFactories) {
+ $io->info('Fixture Factories plugin detected - skipping fixture property generation.');
+ } elseif ($args->getOption('fixtures')) {
+ $fixtures = array_map('trim', explode(',', (string)$args->getOption('fixtures')));
$this->_fixtures = array_filter($fixtures);
} elseif ($this->typeCanDetectFixtures($type) && class_exists($fullClassName)) {
$io->out('Bake is detecting possible fixtures...');
$testSubject = $this->buildTestSubject($type, $fullClassName);
- $this->generateFixtureList($testSubject);
+ if ($testSubject instanceof Table || $testSubject instanceof Controller) {
+ $this->generateFixtureList($testSubject);
+ }
}
}
@@ -267,22 +315,29 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo
[$preConstruct, $construction, $postConstruct] = $this->generateConstructor($type, $fullClassName);
$uses = $this->generateUses($type, $fullClassName);
- $subject = $className;
- [$namespace, $className] = namespaceSplit($fullClassName);
+ // For generic Class type, extract just the class name for the subject
+ if ($type === 'Class') {
+ [$namespace, $className] = namespaceSplit($fullClassName);
+ $subject = $className;
+ } else {
+ $subject = $className;
+ [$namespace, $className] = namespaceSplit($fullClassName);
+ }
$baseNamespace = Configure::read('App.namespace');
if ($this->plugin) {
$baseNamespace = $this->_pluginNamespace($this->plugin);
}
- $subNamespace = substr($namespace, strlen($baseNamespace) + 1);
+ $subNamespace = substr($namespace, strlen((string)$baseNamespace) + 1);
$properties = $this->generateProperties($type, $subject, $fullClassName);
- $io->out("\n" . sprintf('Baking test case for %s ...', $fullClassName), 1, Shell::QUIET);
+ $io->out("\n" . sprintf('Baking test case for %s ...', $fullClassName), 1, ConsoleIo::QUIET);
$contents = $this->createTemplateRenderer()
->set('fixtures', $this->_fixtures)
->set('plugin', $this->plugin)
+ ->set('hasFixtureFactories', $hasFixtureFactories)
->set(compact(
'subject',
'className',
@@ -297,7 +352,7 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo
'uses',
'baseNamespace',
'subNamespace',
- 'namespace'
+ 'namespace',
))
->generate('Bake.tests/test_case');
@@ -311,6 +366,17 @@ public function bake(string $type, string $className, Arguments $args, ConsoleIo
return false;
}
+ /**
+ * Check if the CakePHP Fixture Factories plugin is available
+ *
+ * @return bool
+ */
+ protected function hasFixtureFactories(): bool
+ {
+ return class_exists('CakephpFixtureFactories\Plugin')
+ || class_exists('CakephpFixtureFactories\CakephpFixtureFactoriesPlugin');
+ }
+
/**
* Checks whether the chosen type can find its own fixtures.
* Currently only model, and controller are supported
@@ -331,7 +397,7 @@ public function typeCanDetectFixtures(string $type): bool
* @param string $class The classname of the class the test is being generated for.
* @return object And instance of the class that is going to be tested.
*/
- public function buildTestSubject(string $type, string $class)
+ public function buildTestSubject(string $type, string $class): object
{
if ($type === 'Table') {
[, $name] = namespaceSplit($class);
@@ -347,7 +413,7 @@ public function buildTestSubject(string $type, string $class)
]);
}
} elseif ($type === 'Controller') {
- $instance = new $class(new Request(), new Response());
+ $instance = new $class(new Request());
} else {
$instance = new $class();
}
@@ -370,9 +436,20 @@ public function getRealClassName(string $type, string $class, ?string $prefix =
if ($this->plugin) {
$namespace = str_replace('/', '\\', $this->plugin);
}
+
+ // For generic Class type, the class name contains the full subnamespace path
+ if ($type === 'Class') {
+ // Strip base namespace if user included it
+ if (str_starts_with($class, $namespace . '\\')) {
+ $class = substr($class, strlen((string)$namespace) + 1);
+ }
+
+ return $namespace . '\\' . $class;
+ }
+
$suffix = $this->classSuffixes[$type];
$subSpace = $this->mapType($type);
- if ($suffix && strpos($class, $suffix) === false) {
+ if ($suffix && !str_contains($class, $suffix)) {
$class .= $suffix;
}
if (in_array($type, ['Controller', 'Cell'], true) && $prefix) {
@@ -404,7 +481,7 @@ public function getSubspacePath(string $type): string
*/
public function mapType(string $type): string
{
- if (empty($this->classTypes[$type])) {
+ if (!isset($this->classTypes[$type])) {
throw new CakeException('Invalid object type: ' . $type);
}
@@ -415,8 +492,8 @@ public function mapType(string $type): string
* Get methods declared in the class given.
* No parent methods will be returned
*
- * @param string $className Name of class to look at.
- * @return string[] Array of method names.
+ * @param class-string $className Name of class to look at.
+ * @return array Array of method names.
* @throws \ReflectionException
*/
public function getTestableMethods(string $className): array
@@ -441,18 +518,17 @@ public function getTestableMethods(string $className): array
* loaded models.
*
* @param \Cake\ORM\Table|\Cake\Controller\Controller $subject The object you want to generate fixtures for.
- * @return string[] Array of fixtures to be included in the test.
+ * @return array Array of fixtures to be included in the test.
*/
- public function generateFixtureList($subject): array
+ public function generateFixtureList(Table|Controller $subject): array
{
$this->_fixtures = [];
if ($subject instanceof Table) {
$this->_processModel($subject);
- } elseif ($subject instanceof Controller) {
+ } else {
$this->_processController($subject);
}
- /** @psalm-suppress RedundantFunctionCall */
return array_values($this->_fixtures);
}
@@ -469,9 +545,9 @@ protected function _processModel(Table $subject): void
$assoc = $subject->getAssociation($alias);
$target = $assoc->getTarget();
$name = $target->getAlias();
- $subjectClass = get_class($subject);
+ $subjectClass = $subject::class;
- if ($subjectClass !== Table::class && $subjectClass === get_class($target)) {
+ if ($subjectClass !== Table::class && $subjectClass === $target::class) {
continue;
}
if (!isset($this->_fixtures[$name])) {
@@ -490,8 +566,8 @@ protected function _processModel(Table $subject): void
protected function _processController(Controller $subject): void
{
try {
- $model = $subject->loadModel();
- } catch (UnexpectedValueException $exception) {
+ $model = $subject->fetchTable();
+ } catch (UnexpectedValueException) {
// No fixtures needed or possible
return;
}
@@ -512,11 +588,7 @@ protected function _processController(Controller $subject): void
*/
protected function _addFixture(string $name): void
{
- if ($this->plugin) {
- $prefix = 'plugin.' . $this->plugin . '.';
- } else {
- $prefix = 'app.';
- }
+ $prefix = $this->plugin ? 'plugin.' . $this->plugin . '.' : 'app.';
$fixture = $prefix . $this->_fixtureName($name);
$this->_fixtures[$name] = $fixture;
}
@@ -538,7 +610,7 @@ public function hasMockClass(string $type): bool
*
* @param string $type The Type of object you are generating tests for eg. controller
* @param string $fullClassName The full classname of the class the test is being generated for.
- * @return string[] Constructor snippets for the thing you are building.
+ * @return array Constructor snippets for the thing you are building.
*/
public function generateConstructor(string $type, string $fullClassName): array
{
@@ -561,31 +633,32 @@ public function generateConstructor(string $type, string $fullClassName): array
$pre = '$view = new View();';
$construct = "new {$className}(\$view);";
}
- if ($type === 'Command') {
- $construct = '$this->useCommandRunner();';
- }
if ($type === 'Component') {
$pre = '$registry = new ComponentRegistry();';
$construct = "new {$className}(\$registry);";
}
- if ($type === 'Shell') {
- $pre = "\$this->io = \$this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();";
- $construct = "new {$className}(\$this->io);";
- }
- if ($type === 'Task') {
- $pre = "\$this->io = \$this->getMockBuilder('Cake\Console\ConsoleIo')->getMock();";
- $construct = "new {$className}(\$this->io);";
- }
if ($type === 'Cell') {
$pre = "\$this->request = \$this->getMockBuilder('Cake\Http\ServerRequest')->getMock();\n";
$pre .= " \$this->response = \$this->getMockBuilder('Cake\Http\Response')->getMock();";
$construct = "new {$className}(\$this->request, \$this->response);";
}
- if ($type === 'ShellHelper' || $type === 'CommandHelper') {
+ if ($type === 'CommandHelper') {
$pre = "\$this->stub = new ConsoleOutput();\n";
$pre .= ' $this->io = new ConsoleIo($this->stub);';
$construct = "new {$className}(\$this->io);";
}
+ if ($type === 'Class') {
+ // Check if class has required constructor parameters
+ if (class_exists($fullClassName)) {
+ $reflection = new ReflectionClass($fullClassName);
+ $constructor = $reflection->getConstructor();
+ if (!$constructor || $constructor->getNumberOfRequiredParameters() === 0) {
+ $construct = "new {$className}();";
+ }
+ } else {
+ $construct = "new {$className}();";
+ }
+ }
return [$pre, $construct, $post];
}
@@ -622,17 +695,7 @@ public function generateProperties(string $type, string $subject, string $fullCl
];
break;
- case 'Shell':
- case 'Task':
- $properties[] = [
- 'description' => 'ConsoleIo mock',
- 'type' => '\Cake\Console\ConsoleIo|\PHPUnit\Framework\MockObject\MockObject',
- 'name' => 'io',
- ];
- break;
-
case 'CommandHelper':
- case 'ShellHelper':
$properties[] = [
'description' => 'ConsoleOutput stub',
'type' => '\Cake\TestSuite\Stub\ConsoleOutput',
@@ -646,7 +709,17 @@ public function generateProperties(string $type, string $subject, string $fullCl
break;
}
- if (!in_array($type, ['Controller', 'Command'])) {
+ // Skip test subject property for Controller, Command, and Class types with required constructor params
+ $skipProperty = in_array($type, ['Controller', 'Command'], true);
+ if ($type === 'Class' && class_exists($fullClassName)) {
+ $reflection = new ReflectionClass($fullClassName);
+ $constructor = $reflection->getConstructor();
+ if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
+ $skipProperty = true;
+ }
+ }
+
+ if (!$skipProperty) {
$properties[] = [
'description' => 'Test subject',
'type' => '\\' . $fullClassName,
@@ -662,7 +735,7 @@ public function generateProperties(string $type, string $subject, string $fullCl
*
* @param string $type The Type of object you are generating tests for eg. controller
* @param string $fullClassName The Classname of the class the test is being generated for.
- * @return string[] An array containing used classes
+ * @return array An array containing used classes
*/
public function generateUses(string $type, string $fullClassName): array
{
@@ -673,7 +746,7 @@ public function generateUses(string $type, string $fullClassName): array
if ($type === 'Helper') {
$uses[] = 'Cake\View\View';
}
- if ($type === 'ShellHelper' || $type === 'CommandHelper') {
+ if ($type === 'CommandHelper') {
$uses[] = 'Cake\TestSuite\Stub\ConsoleOutput';
$uses[] = 'Cake\Console\ConsoleIo';
}
@@ -693,12 +766,11 @@ public function generateUses(string $type, string $fullClassName): array
public function getBasePath(): string
{
$dir = 'TestCase/';
- $path = defined('TESTS') ? TESTS . $dir : ROOT . DS . 'tests' . DS . $dir;
if ($this->plugin) {
- $path = $this->_pluginPath($this->plugin) . 'tests/' . $dir;
+ return $this->_pluginPath($this->plugin) . 'tests/' . $dir;
}
- return $path;
+ return defined('TESTS') ? TESTS . $dir : ROOT . DS . 'tests' . DS . $dir;
}
/**
@@ -717,7 +789,7 @@ public function testCaseFileName(string $type, string $className): string
$namespace = $this->plugin;
}
- $classTail = substr($className, strlen($namespace) + 1);
+ $classTail = substr($className, strlen((string)$namespace) + 1);
$path = $path . $classTail . 'Test.php';
return str_replace(['/', '\\'], DS, $path);
@@ -729,15 +801,15 @@ public function testCaseFileName(string $type, string $className): string
* @param \Cake\Console\ConsoleOptionParser $parser Option parser to update
* @return \Cake\Console\ConsoleOptionParser
*/
- public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
+ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser = $this->_setCommonOptions($parser);
$types = array_keys($this->classTypes);
- $types = array_merge($types, array_map([$this, 'underscore'], $types));
+ $types = array_merge($types, array_map($this->underscore(...), $types));
$parser->setDescription(
- 'Bake test case skeletons for classes.'
+ 'Bake test case skeletons for classes.',
)->addArgument('type', [
'help' => 'Type of class to bake, can be any of the following:' .
' controller, model, helper, component or behavior.',
diff --git a/app/vendor/cakephp/bake/src/Plugin.php b/app/vendor/cakephp/bake/src/Plugin.php
deleted file mode 100644
index ec8703b03..000000000
--- a/app/vendor/cakephp/bake/src/Plugin.php
+++ /dev/null
@@ -1,159 +0,0 @@
-addPlugin('Cake/TwigView');
- }
-
- /**
- * Define the console commands for an application.
- *
- * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
- * @return \Cake\Console\CommandCollection The updated collection.
- */
- public function console(CommandCollection $commands): CommandCollection
- {
- // Add commands in plugins and app.
- $commands = $this->discoverCommands($commands);
-
- // Add entry command to handle entry point and backwards compat.
- $commands->add(EntryCommand::defaultName(), EntryCommand::class);
-
- return $commands;
- }
-
- /**
- * Scan plugins and application to find commands that are intended
- * to be used with bake.
- *
- * Non-Abstract commands extending `Bake\Command\BakeCommand` are included.
- * Plugins are scanned in the order they are listed in `Plugin::loaded()`
- *
- * @param \Cake\Console\CommandCollection $commands The CommandCollection to add commands into.
- * @return \Cake\Console\CommandCollection The updated collection.
- */
- protected function discoverCommands(CommandCollection $commands): CommandCollection
- {
- foreach (CorePlugin::getCollection()->with('console') as $plugin) {
- $namespace = str_replace('/', '\\', $plugin->getName());
- $pluginPath = $plugin->getClassPath();
-
- $found = $this->findInPath($namespace, $pluginPath);
- if (count($found)) {
- $commands->addMany($found);
- }
- }
-
- $found = $this->findInPath(Configure::read('App.namespace'), APP);
- if (count($found)) {
- $commands->addMany($found);
- }
-
- return $commands;
- }
-
- /**
- * Search a path for commands.
- *
- * @param string $namespace The namespace classes are expected to be in.
- * @param string $path The path to look in.
- * @return string[]
- * @psalm-return array>
- */
- protected function findInPath(string $namespace, string $path): array
- {
- $hasSubfolder = false;
- $path .= 'Command/';
- $namespace .= '\Command\\';
-
- if (file_exists($path . 'Bake/')) {
- $hasSubfolder = true;
- $path .= 'Bake/';
- $namespace .= 'Bake\\';
- } elseif (!file_exists($path)) {
- return [];
- }
-
- $iterator = new DirectoryIterator($path);
- $candidates = [];
- foreach ($iterator as $item) {
- if ($item->isDot() || $item->isDir()) {
- continue;
- }
- /** @psalm-var class-string<\Bake\Command\BakeCommand> $class */
- $class = $namespace . $item->getBasename('.php');
-
- if (!$hasSubfolder) {
- try {
- $reflection = new ReflectionClass($class);
- /** @phpstan-ignore-next-line */
- } catch (ReflectionException $e) {
- continue;
- }
- if (!$reflection->isInstantiable() || !$reflection->isSubclassOf(BakeCommand::class)) {
- continue;
- }
- }
-
- $candidates[$class::defaultName()] = $class;
- }
-
- return $candidates;
- }
-}
diff --git a/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php b/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php
deleted file mode 100644
index 8d2a4b909..000000000
--- a/app/vendor/cakephp/bake/src/Shell/Task/BakeTask.php
+++ /dev/null
@@ -1,198 +0,0 @@
-connection) && !empty($this->params['connection'])) {
- $this->connection = $this->params['connection'];
- }
- }
-
- /**
- * Get the prefix name.
- *
- * Handles camelcasing each namespace in the prefix path.
- *
- * @return string The inflected prefix path.
- */
- protected function _getPrefix(): string
- {
- $prefix = $this->param('prefix');
- if (!$prefix) {
- return '';
- }
- $parts = explode('/', $prefix);
-
- return implode('/', array_map([$this, '_camelize'], $parts));
- }
-
- /**
- * Gets the path for output. Checks the plugin property
- * and returns the correct path.
- *
- * @return string Path to output.
- */
- public function getPath(): string
- {
- $path = APP . $this->pathFragment;
- if ($this->plugin) {
- $path = $this->_pluginPath($this->plugin) . 'src/' . $this->pathFragment;
- }
- $prefix = $this->_getPrefix();
- if ($prefix) {
- $path .= $prefix . DS;
- }
-
- return str_replace('/', DS, $path);
- }
-
- /**
- * Base execute method parses some parameters and sets some properties on the bake tasks.
- * call when overriding execute()
- *
- * @return int|null
- */
- public function main()
- {
- if (isset($this->params['plugin'])) {
- $parts = explode('/', $this->params['plugin']);
- $this->plugin = implode('/', array_map([$this, '_camelize'], $parts));
- if (strpos($this->plugin, '\\')) {
- $this->abort('Invalid plugin namespace separator, please use / instead of \ for plugins.');
- }
- }
- if (isset($this->params['connection'])) {
- $this->connection = $this->params['connection'];
- }
-
- return static::CODE_SUCCESS;
- }
-
- /**
- * Executes an external shell command and pipes its output to the stdout
- *
- * @param string $command the command to execute
- * @return void
- * @throws \RuntimeException if any errors occurred during the execution
- */
- public function callProcess(string $command): void
- {
- $process = new Process($this->_io);
- $out = $process->call($command);
- $this->out($out);
- }
-
- /**
- * Handles splitting up the plugin prefix and classname.
- *
- * Sets the plugin parameter and plugin property.
- *
- * @param string $name The name to possibly split.
- * @return string The name without the plugin prefix.
- */
- protected function _getName(string $name): string
- {
- if (empty($name)) {
- return $name;
- }
-
- if (strpos($name, '.')) {
- [$plugin, $name] = pluginSplit($name);
- $this->plugin = $this->params['plugin'] = $plugin;
- }
-
- return $name;
- }
-
- /**
- * Delete empty file in a given path
- *
- * @param string $path Path to folder which contains 'empty' file.
- * @return void
- */
- protected function _deleteEmptyFile(string $path): void
- {
- if (file_exists($path)) {
- unlink($path);
- $this->out(sprintf('Deleted `%s`', $path), 1, Shell::QUIET);
- }
- }
-
- /**
- * Get the option parser for this task.
- *
- * This base class method sets up some commonly used options.
- *
- * @return \Cake\Console\ConsoleOptionParser
- */
- public function getOptionParser(): ConsoleOptionParser
- {
- return $this->_setCommonOptions(parent::getOptionParser());
- }
-}
diff --git a/app/vendor/cakephp/bake/src/Shell/Task/SimpleBakeTask.php b/app/vendor/cakephp/bake/src/Shell/Task/SimpleBakeTask.php
deleted file mode 100644
index efadac3cb..000000000
--- a/app/vendor/cakephp/bake/src/Shell/Task/SimpleBakeTask.php
+++ /dev/null
@@ -1,128 +0,0 @@
-
- */
- public function templateData(): array
- {
- $namespace = Configure::read('App.namespace');
- if ($this->plugin) {
- $namespace = $this->_pluginNamespace($this->plugin);
- }
-
- return ['namespace' => $namespace];
- }
-
- /**
- * Execute method
- *
- * @param string|null $name The name of the object to bake.
- * @return int|null
- */
- public function main(?string $name = null): ?int
- {
- parent::main();
- if (empty($name)) {
- $this->abort('You must provide a name to bake a ' . $this->name());
- }
- $name = $this->_getName($name);
- $name = Inflector::camelize($name);
- $this->bake($name);
-
- return static::CODE_SUCCESS;
- }
-
- /**
- * Generate a class stub
- *
- * @param string $name The classname to generate.
- * @return string
- */
- public function bake(string $name): string
- {
- $renderer = new TemplateRenderer($this->param('theme'));
- $renderer->set('name', $name);
- $renderer->set($this->templateData());
- $contents = $renderer->generate($this->template());
-
- $filename = $this->getPath() . $this->fileName($name);
- $this->createFile($filename, $contents);
- $emptyFile = $this->getPath() . '.gitkeep';
- $this->_deleteEmptyFile($emptyFile);
-
- return $contents;
- }
-
- /**
- * Gets the option parser instance and configures it.
- *
- * @return \Cake\Console\ConsoleOptionParser
- */
- public function getOptionParser(): ConsoleOptionParser
- {
- $parser = parent::getOptionParser();
- $name = $this->name();
- $parser->setDescription(
- sprintf('Bake a %s class file.', $name)
- )->addArgument('name', [
- 'help' => sprintf(
- 'Name of the %s to bake. Can use Plugin.name to bake %s files into plugins.',
- $name,
- $name
- ),
- ]);
-
- return $parser;
- }
-}
diff --git a/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php b/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php
index 07647b4e6..29541216d 100644
--- a/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php
+++ b/app/vendor/cakephp/bake/src/Utility/CommonOptionsTrait.php
@@ -30,25 +30,13 @@
*/
trait CommonOptionsTrait
{
- /**
- * @var string
- */
- public $plugin;
+ public ?string $plugin = null;
- /**
- * @var string|null
- */
- public $theme;
+ public ?string $theme = null;
- /**
- * @var string
- */
- public $connection;
+ public string $connection;
- /**
- * @var bool
- */
- public $force = false;
+ public bool $force = false;
/**
* Pull common/frequently used arguments & options into properties
@@ -62,20 +50,22 @@ protected function extractCommonProperties(Arguments $args): void
// These properties should ideally not exist, but until ConsoleOptionParser
// gets validation and transform logic they will have to stay.
if ($args->hasOption('plugin')) {
- $plugin = $args->getOption('plugin');
+ $plugin = (string)$args->getOption('plugin');
$parts = explode('/', $plugin);
$this->plugin = implode('/', array_map([$this, '_camelize'], $parts));
if (strpos($this->plugin, '\\')) {
throw new InvalidArgumentException(
- 'Invalid plugin namespace separator, please use / instead of \ for plugins.'
+ 'Invalid plugin namespace separator, please use / instead of \ for plugins.',
);
}
}
- $this->theme = $args->getOption('theme');
- $this->connection = $args->getOption('connection');
- $this->force = $args->getOption('force');
+ $theme = $args->getOption('theme');
+ $this->theme = is_string($theme) ? $theme : null;
+ $connection = $args->getOption('connection');
+ $this->connection = is_string($connection) ? $connection : 'default';
+ $this->force = (bool)$args->getOption('force');
}
/**
diff --git a/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php b/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php
index 643d68b62..db3d16923 100644
--- a/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php
+++ b/app/vendor/cakephp/bake/src/Utility/Model/AssociationFilter.php
@@ -19,6 +19,7 @@
use Cake\ORM\Table;
use Cake\Utility\Inflector;
use Exception;
+use function Cake\Core\namespaceSplit;
/**
* Utility class to filter Model Table associations
@@ -30,8 +31,8 @@ class AssociationFilter
* belongsToMany associations provided
*
* @param \Cake\ORM\Table $table Table
- * @param string[] $aliases array of aliases
- * @return string[] $aliases
+ * @param array $aliases array of aliases
+ * @return array $aliases
*/
public function filterHasManyAssociationsAliases(Table $table, array $aliases): array
{
@@ -44,7 +45,7 @@ public function filterHasManyAssociationsAliases(Table $table, array $aliases):
* Get the array of junction aliases for all the BelongsToMany associations
*
* @param \Cake\ORM\Table $table Table
- * @return string[] Junction aliases of all the BelongsToMany associations
+ * @return array Junction aliases of all the BelongsToMany associations
*/
public function belongsToManyJunctionAliases(Table $table): array
{
@@ -77,11 +78,11 @@ public function filterAssociations(Table $model): array
if ($type === 'HasMany' && in_array($alias, $belongsToManyJunctionsAliases)) {
continue;
}
- $targetClass = get_class($target);
+ $targetClass = $target::class;
[, $className] = namespaceSplit($targetClass);
$navLink = true;
- $modelClass = get_class($model);
+ $modelClass = $model::class;
if ($modelClass !== Table::class && $targetClass === $modelClass) {
$navLink = false;
}
@@ -92,6 +93,7 @@ public function filterAssociations(Table $model): array
}
try {
+ $foreignKey = (array)$assoc->getForeignKey();
$associations[$type][$assocName] = [
'property' => $assoc->getProperty(),
'variable' => Inflector::variable($assocName),
@@ -100,10 +102,10 @@ public function filterAssociations(Table $model): array
'foreignKey' => $assoc->getForeignKey(),
'alias' => $alias,
'controller' => $className,
- 'fields' => $target->getSchema()->columns(),
+ 'fields' => array_values(array_diff($target->getSchema()->columns(), $foreignKey)),
'navLink' => $navLink,
];
- } catch (Exception $e) {
+ } catch (Exception) {
// Do nothing it could be a bogus association name.
}
}
diff --git a/app/vendor/cakephp/bake/src/Utility/Model/EnumParser.php b/app/vendor/cakephp/bake/src/Utility/Model/EnumParser.php
new file mode 100644
index 000000000..ac80024b8
--- /dev/null
+++ b/app/vendor/cakephp/bake/src/Utility/Model/EnumParser.php
@@ -0,0 +1,60 @@
+
+ */
+ public static function parseCases(?string $casesString, bool $int): array
+ {
+ if ($casesString === null || $casesString === '') {
+ return [];
+ }
+
+ $enumCases = explode(',', $casesString);
+
+ $definition = [];
+ foreach ($enumCases as $k => $enumCase) {
+ $case = $value = trim($enumCase);
+ $pos = strpos($case, ':');
+ if ($pos !== false) {
+ $value = trim(mb_substr($case, $pos + 1));
+ $case = mb_substr($case, 0, $pos);
+ } elseif ($int) {
+ $value = $k;
+ }
+
+ if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $case)) {
+ throw new InvalidArgumentException(sprintf('`%s` is not a valid enum case', $case));
+ }
+ if (is_string($value) && str_contains($value, "'")) {
+ throw new InvalidArgumentException(sprintf("`%s` value cannot contain `'` character", $case));
+ }
+
+ $definition[$case] = $int ? (int)$value : $value;
+ }
+
+ return $definition;
+ }
+
+ /**
+ * Parses an enum definition from a DB column comment.
+ *
+ * @return string
+ */
+ public static function parseDefinitionString(string $comment): string
+ {
+ $string = trim(mb_substr($comment, strpos($comment, '[enum]') + 6));
+ $pos = strpos($string, ';');
+ if ($pos !== false) {
+ return trim(mb_substr($string, 0, $pos));
+ }
+
+ return $string;
+ }
+}
diff --git a/app/vendor/cakephp/bake/src/Utility/Process.php b/app/vendor/cakephp/bake/src/Utility/Process.php
index 974e0ea4a..27c9cc155 100644
--- a/app/vendor/cakephp/bake/src/Utility/Process.php
+++ b/app/vendor/cakephp/bake/src/Utility/Process.php
@@ -26,10 +26,7 @@
*/
class Process
{
- /**
- * @var \Cake\Console\ConsoleIo
- */
- protected $io;
+ protected ConsoleIo $io;
/**
* Constructor
@@ -59,10 +56,10 @@ public function call(string $command): string
$process = proc_open(
$command,
$descriptorSpec,
- $pipes
+ $pipes,
);
if (!is_resource($process)) {
- throw new RuntimeException("Could not start subprocess for `$command`");
+ throw new RuntimeException("Could not start subprocess for `{$command}`");
}
fclose($pipes[0]);
@@ -74,7 +71,7 @@ public function call(string $command): string
$exit = proc_close($process);
if ($exit !== 0) {
- throw new \RuntimeException($error);
+ throw new RuntimeException($error);
}
return $output;
diff --git a/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php b/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php
index f05c13a10..995a31e65 100644
--- a/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php
+++ b/app/vendor/cakephp/bake/src/Utility/SubsetSchemaCollection.php
@@ -27,19 +27,16 @@
*/
class SubsetSchemaCollection implements CollectionInterface
{
- /**
- * @var \Cake\Database\Schema\CollectionInterface
- */
- protected $collection;
+ protected CollectionInterface $collection;
/**
- * @var string[]
+ * @var array
*/
- protected $tables = [];
+ protected array $tables = [];
/**
* @param \Cake\Database\Schema\CollectionInterface $collection The wrapped collection
- * @param string[] $tables The subset of tables.
+ * @param list $tables The subset of tables.
*/
public function __construct(CollectionInterface $collection, array $tables)
{
@@ -60,7 +57,7 @@ public function getInnerCollection(): CollectionInterface
/**
* Get the list of tables in this schema collection.
*
- * @return string[]
+ * @return array
*/
public function listTables(): array
{
diff --git a/app/vendor/cakephp/bake/src/Utility/TableScanner.php b/app/vendor/cakephp/bake/src/Utility/TableScanner.php
index 2b327c3f6..ace2b00ca 100644
--- a/app/vendor/cakephp/bake/src/Utility/TableScanner.php
+++ b/app/vendor/cakephp/bake/src/Utility/TableScanner.php
@@ -29,28 +29,25 @@
*/
class TableScanner
{
- /**
- * @var \Cake\Database\Connection
- */
- protected $connection;
+ protected Connection $connection;
/**
- * @var string[]
+ * @var array
*/
- protected $ignore;
+ protected array $ignore;
/**
* Constructor
*
* @param \Cake\Database\Connection $connection The connection name in ConnectionManager
- * @param string[]|null $ignore List of tables or regex pattern to ignore. If null, the default ignore
+ * @param array|null $ignore List of tables or regex pattern to ignore. If null, the default ignore
* list will be used.
*/
public function __construct(Connection $connection, ?array $ignore = null)
{
$this->connection = $connection;
if ($ignore === null) {
- $ignore = ['i18n', 'cake_sessions', 'sessions', '/phinxlog/'];
+ $ignore = ['i18n', 'cake_sessions', 'cake_migrations', 'cake_seeds', 'sessions', '/phinxlog/'];
}
$this->ignore = $ignore;
}
@@ -58,13 +55,13 @@ public function __construct(Connection $connection, ?array $ignore = null)
/**
* Get all tables in the connection without applying ignores.
*
- * @return string[]
+ * @return array
*/
public function listAll(): array
{
$schema = $this->connection->getSchemaCollection();
$tables = $schema->listTables();
- if (empty($tables)) {
+ if (!$tables) {
throw new RuntimeException('Your database does not have any tables.');
}
sort($tables);
@@ -75,7 +72,7 @@ public function listAll(): array
/**
* Get all tables in the connection that aren't ignored.
*
- * @return string[]
+ * @return array
*/
public function listUnskipped(): array
{
@@ -90,6 +87,29 @@ public function listUnskipped(): array
return $tables;
}
+ /**
+ * Call from any All command that needs the shadow translation tables to be skipped.
+ *
+ * @param array $tables
+ * @return array
+ */
+ public function removeShadowTranslationTables(array $tables): array
+ {
+ foreach ($tables as $key => $table) {
+ if (!preg_match('/^(.+)_translations$/', $table, $matches)) {
+ continue;
+ }
+
+ if (empty($tables[$matches[1]])) {
+ continue;
+ }
+
+ unset($tables[$key]);
+ }
+
+ return $tables;
+ }
+
/**
* @param string $table Table name.
* @return bool
@@ -97,10 +117,8 @@ public function listUnskipped(): array
protected function shouldSkip(string $table): bool
{
foreach ($this->ignore as $ignore) {
- if (strpos($ignore, '/') === 0) {
- if ((bool)preg_match($ignore, $table)) {
- return true;
- }
+ if (str_starts_with($ignore, '/') && (bool)preg_match($ignore, $table)) {
+ return true;
}
if ($ignore === $table) {
diff --git a/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php b/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php
index 027934b89..7566cd070 100644
--- a/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php
+++ b/app/vendor/cakephp/bake/src/Utility/TemplateRenderer.php
@@ -34,17 +34,13 @@ class TemplateRenderer
/**
* BakeView instance
- *
- * @var \Bake\View\BakeView|null
*/
- protected $view;
+ protected ?BakeView $view = null;
/**
* Template theme
- *
- * @var string|null
*/
- protected $theme;
+ protected ?string $theme;
/**
* Constructor
@@ -59,12 +55,12 @@ public function __construct(?string $theme = null)
/**
* Get view instance
*
- * @return \Cake\View\View
+ * @return \Bake\View\BakeView
* @triggers Bake.initialize $view
*/
public function getView(): View
{
- if ($this->view) {
+ if ($this->view instanceof BakeView) {
return $this->view;
}
@@ -99,7 +95,7 @@ public function generate(string $template, ?array $vars = null): string
try {
return $view->render($template);
- } catch (MissingTemplateException $e) {
+ } catch (MissingTemplateException) {
$message = sprintf('No bake template found for "%s" skipping file generation.', $template);
throw new MissingTemplateException($message);
}
diff --git a/app/vendor/cakephp/bake/src/View/BakeView.php b/app/vendor/cakephp/bake/src/View/BakeView.php
index 3a3b6df78..ebc623dfa 100644
--- a/app/vendor/cakephp/bake/src/View/BakeView.php
+++ b/app/vendor/cakephp/bake/src/View/BakeView.php
@@ -20,6 +20,7 @@
use Cake\Core\ConventionsTrait;
use Cake\Event\EventInterface;
use Cake\TwigView\View\TwigView;
+use function Cake\Core\pluginSplit;
class BakeView extends TwigView
{
@@ -35,7 +36,7 @@ class BakeView extends TwigView
/**
* @inheritDoc
*/
- protected $layout = 'Bake.default';
+ protected string $layout = 'Bake.default';
/**
* Initialize view
@@ -73,8 +74,10 @@ public function initialize(): void
* @throws \Cake\Core\Exception\CakeException If there is an error in the view.
* @return string Rendered content.
*/
- public function render(?string $template = null, $layout = null): string
+ public function render(?string $template = null, string|false|null $layout = null): string
{
+ assert($template !== null, 'Template name must be provided.');
+
$viewFileName = $this->_getTemplateFileName($template);
[, $templateEventName] = pluginSplit($template);
$templateEventName = str_replace(['/', '\\'], '.', $templateEventName);
@@ -102,17 +105,18 @@ public function render(?string $template = null, $layout = null): string
* Use the Bake prefix for bake related view events
*
* @param string $name Name of the event.
- * @param mixed $data Any value you wish to be transported with this event to
+ * @param array $data Any value you wish to be transported with this event to
* it can be read by listeners.
- *
- * @param mixed $subject The object that this event applies to
+ * @param object|null $subject The object that this event applies to
* ($this by default).
- * @return \Cake\Event\EventInterface
+ * @return \Cake\Event\EventInterface
+ * @phpstan-ignore missingType.generics
*/
- public function dispatchEvent(string $name, $data = null, $subject = null): EventInterface
+ public function dispatchEvent(string $name, array $data = [], ?object $subject = null): EventInterface
{
- $name = preg_replace('/^View\./', 'Bake.', $name);
+ $name = (string)preg_replace('/^View\./', 'Bake.', $name);
+ /** @phpstan-ignore-next-line missingType.generics */
return parent::dispatchEvent($name, $data, $subject);
}
@@ -121,14 +125,14 @@ public function dispatchEvent(string $name, $data = null, $subject = null): Even
*
* @param ?string $plugin Optional plugin name to scan for view files.
* @param bool $cached Set to false to force a refresh of view paths. Default true.
- * @return string[] paths
+ * @return array paths
*/
protected function _paths(?string $plugin = null, bool $cached = true): array
{
$paths = parent::_paths($plugin, false);
foreach ($paths as &$path) {
// Append 'bake' to all directories that aren't the application override directory.
- if (strpos($path, 'plugin' . DS . 'Bake') === false) {
+ if (!str_contains($path, 'plugin' . DS . 'Bake')) {
$path .= static::BAKE_TEMPLATE_FOLDER . DS;
}
}
diff --git a/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php b/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php
index 8dc0d899f..e73388c64 100644
--- a/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php
+++ b/app/vendor/cakephp/bake/src/View/Helper/BakeHelper.php
@@ -8,14 +8,21 @@
use Brick\VarExporter\VarExporter;
use Cake\Core\Configure;
use Cake\Core\ConventionsTrait;
+use Cake\Core\Plugin;
use Cake\Database\Schema\TableSchema;
+use Cake\Database\Type\EnumType;
+use Cake\Database\TypeFactory;
use Cake\Datasource\SchemaInterface;
use Cake\ORM\Table;
use Cake\Utility\Inflector;
use Cake\View\Helper;
+use function Cake\Collection\collection;
+use function Cake\Core\pluginSplit;
/**
* Bake helper
+ *
+ * @extends \Cake\View\Helper<\Cake\View\View>
*/
class BakeHelper extends Helper
{
@@ -26,14 +33,12 @@ class BakeHelper extends Helper
*
* @var array
*/
- protected $_defaultConfig = [];
+ protected array $_defaultConfig = [];
/**
* AssociationFilter utility
- *
- * @var \Bake\Utility\Model\AssociationFilter|null
*/
- protected $_associationFilter = null;
+ protected ?AssociationFilter $_associationFilter = null;
/**
* Used for generating formatted properties such as component and helper arrays
@@ -60,74 +65,6 @@ public function arrayProperty(string $name, array $value = [], array $options =
return $this->_View->element('array_property', $options);
}
- /**
- * Returns an array converted into a formatted multiline string
- *
- * @param array $list array of items to be stringified
- * @param array $options options to use
- * @return string
- * @deprecated 2.5.0 Use BakeHelper::exportVar() instead.
- */
- public function stringifyList(array $list, array $options = []): string
- {
- $defaults = [
- 'indent' => 2,
- 'tab' => ' ',
- 'trailingComma' => !isset($options['indent']) || $options['indent'] ? true : false,
- 'quotes' => true,
- ];
- $options += $defaults;
-
- if (!$list) {
- return '';
- }
-
- foreach ($list as $k => &$v) {
- if ($options['quotes']) {
- $v = "'$v'";
- }
- if (!is_numeric($k)) {
- $nestedOptions = $options;
- if ($nestedOptions['indent']) {
- $nestedOptions['indent'] += 1;
- }
- if (is_array($v)) {
- $v = sprintf(
- "'%s' => [%s]",
- $k,
- $this->stringifyList($v, $nestedOptions)
- );
- } else {
- $v = "'$k' => $v";
- }
- } elseif (is_array($v)) {
- $nestedOptions = $options;
- if ($nestedOptions['indent']) {
- $nestedOptions['indent'] += 1;
- }
- $v = sprintf(
- '[%s]',
- $this->stringifyList($v, $nestedOptions)
- );
- }
- }
-
- $start = $end = '';
- $join = ', ';
- if ($options['indent']) {
- $join = ',';
- $start = "\n" . str_repeat($options['tab'], $options['indent']);
- $join .= $start;
- $end = "\n" . str_repeat($options['tab'], $options['indent'] - 1);
- }
-
- if ($options['trailingComma'] && $options['indent'] > 0) {
- $end = ',' . $end;
- }
-
- return $start . implode($join, $list) . $end;
- }
-
/**
* Export variable to string representation.
*
@@ -140,7 +77,7 @@ public function stringifyList(array $list, array $options = []): string
* @throws \Brick\VarExporter\ExportException
* @see https://github.com/brick/varexporter#options
*/
- public function exportVar($var, int $indentLevel = 0, int $options = 0): string
+ public function exportVar(mixed $var, int $indentLevel = 0, int $options = 0): string
{
$options |= VarExporter::TRAILING_COMMA_IN_ARRAY;
@@ -161,7 +98,7 @@ public function exportArray(array $var, int $indentLevel = 0, bool $inline = tru
{
$options = 0;
if ($inline) {
- $options = VarExporter::INLINE_NUMERIC_SCALAR_ARRAY;
+ $options = VarExporter::INLINE_SCALAR_LIST;
}
return $this->exportVar($var, $indentLevel, $options);
@@ -173,7 +110,7 @@ public function exportArray(array $var, int $indentLevel = 0, bool $inline = tru
*
* @param \Cake\ORM\Table $table object to find associations on
* @param string $assoc association to extract
- * @return string[]
+ * @return array
*/
public function aliasExtractor(Table $table, string $assoc): array
{
@@ -207,13 +144,13 @@ public function aliasExtractor(Table $table, string $assoc): array
*/
public function classInfo(string $class, string $type, string $suffix): array
{
- [$plugin, $name] = \pluginSplit($class);
+ [$plugin, $name] = pluginSplit($class);
$base = Configure::read('App.namespace');
if ($plugin !== null) {
$base = $plugin;
}
- $base = str_replace('/', '\\', trim($base, '\\'));
+ $base = str_replace('/', '\\', trim((string)$base, '\\'));
$sub = '\\' . str_replace('/', '\\', trim($type, '\\'));
$qn = $sub . '\\' . $name . $suffix;
@@ -231,6 +168,16 @@ public function classInfo(string $class, string $type, string $suffix): array
];
}
+ /**
+ * Check if the current application has a plugin installed
+ *
+ * @param string $plugin The plugin name to check for.
+ */
+ public function hasPlugin(string $plugin): bool
+ {
+ return Plugin::isLoaded($plugin);
+ }
+
/**
* Return list of fields to generate controls for.
*
@@ -245,16 +192,16 @@ public function filterFields(
array $fields,
SchemaInterface $schema,
?Table $modelObject = null,
- $takeFields = 0,
- array $filterTypes = ['binary']
+ string|int $takeFields = 0,
+ array $filterTypes = ['binary'],
): array {
$fields = collection($fields)
- ->filter(function ($field) use ($schema, $filterTypes) {
+ ->filter(function ($field) use ($schema, $filterTypes): bool {
return !in_array($schema->getColumnType($field), $filterTypes);
});
if (isset($modelObject) && $modelObject->hasBehavior('Tree')) {
- $fields = $fields->reject(function ($field) {
+ $fields = $fields->reject(function ($field): bool {
return $field === 'lft' || $field === 'rght';
});
}
@@ -279,26 +226,29 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array
$immediateAssociations = $associations['BelongsTo'];
$associationFields = collection($fields)
->map(function ($field) use ($immediateAssociations) {
- foreach ($immediateAssociations as $alias => $details) {
+ foreach ($immediateAssociations as $details) {
if ($field === $details['foreignKey']) {
return [$field => $details];
}
}
})
->filter()
- ->reduce(function ($fields, $value) {
+ ->reduce(function ($fields, $value): float|int|array {
return $fields + $value;
}, []);
$groupedFields = collection($fields)
- ->filter(function ($field) use ($schema) {
+ ->filter(function ($field) use ($schema): bool {
return $schema->getColumnType($field) !== 'binary';
})
- ->groupBy(function ($field) use ($schema, $associationFields) {
+ ->groupBy(function ($field) use ($schema, $associationFields): string {
$type = $schema->getColumnType($field);
if (isset($associationFields[$field])) {
return 'string';
}
+ if ($type && str_starts_with($type, 'enum-')) {
+ return 'enum';
+ }
$numberTypes = ['decimal', 'biginteger', 'integer', 'float', 'smallinteger', 'tinyinteger'];
if (in_array($type, $numberTypes, true)) {
return 'number';
@@ -312,11 +262,11 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array
'timestampfractional',
'timestamptimezone',
];
- if (in_array($type, $dateTypes)) {
+ if (in_array($type, $dateTypes, true)) {
return 'date';
}
- return in_array($type, ['text', 'boolean']) ? $type : 'string';
+ return in_array($type, ['text', 'boolean'], true) ? $type : 'string';
})
->toArray();
@@ -324,6 +274,7 @@ public function getViewFieldsData(array $fields, SchemaInterface $schema, array
'number' => [],
'string' => [],
'boolean' => [],
+ 'enum' => [],
'date' => [],
'text' => [],
];
@@ -343,6 +294,26 @@ public function columnData(string $field, TableSchema $schema): ?array
return $schema->getColumn($field);
}
+ /**
+ * Check if a column is both an enum, and the mapped enum implements `label()` as a method.
+ *
+ * @param string $field the field to check
+ * @param \Cake\Database\Schema\TableSchema $schema The table schema to read from.
+ * @return bool
+ */
+ public function enumSupportsLabel(string $field, TableSchema $schema): bool
+ {
+ $typeName = $schema->getColumnType($field);
+ if (!$typeName || !str_starts_with($typeName, 'enum-')) {
+ return false;
+ }
+ $type = TypeFactory::build($typeName);
+ assert($type instanceof EnumType);
+ $enumClass = $type->getEnumClassName();
+
+ return method_exists($enumClass, 'label');
+ }
+
/**
* Get alias of associated table.
*
@@ -362,7 +333,7 @@ public function getAssociatedTableAlias(Table $modelObj, string $assoc): string
*
* @param string $field Field name.
* @param array $rules Validation rules list.
- * @return string[]
+ * @return array
*/
public function getValidationMethods(string $field, array $rules): array
{
@@ -380,7 +351,7 @@ public function getValidationMethods(string $field, array $rules): array
$field,
$ruleName,
$rule['rule'],
- $rule['provider']
+ $rule['provider'],
);
continue;
}
@@ -389,23 +360,23 @@ public function getValidationMethods(string $field, array $rules): array
$validationMethods[] = sprintf(
"->%s('%s')",
$rule['rule'],
- $field
+ $field,
);
continue;
}
- $rule['args'] = array_map(function ($item) {
+ $rule['args'] = array_map(function ($item): string {
return $this->exportVar(
$item,
is_array($item) ? 3 : 0,
- VarExporter::INLINE_NUMERIC_SCALAR_ARRAY
+ VarExporter::INLINE_SCALAR_LIST,
);
}, $rule['args']);
$validationMethods[] = sprintf(
"->%s('%s', %s)",
$rule['rule'],
$field,
- implode(', ', $rule['args'])
+ implode(', ', $rule['args']),
);
}
@@ -415,11 +386,11 @@ public function getValidationMethods(string $field, array $rules): array
/**
* Get field accessibility data.
*
- * @param string[]|false|null $fields Fields list.
- * @param string[]|null $primaryKey Primary key.
+ * @param array|false|null $fields Fields list.
+ * @param array|null $primaryKey Primary key.
* @return array
*/
- public function getFieldAccessibility($fields = null, $primaryKey = null): array
+ public function getFieldAccessibility(array|false|null $fields = null, ?array $primaryKey = null): array
{
$accessible = [];
@@ -450,7 +421,7 @@ public function escapeArguments(array $args): array
return array_map(function ($v) {
if (is_string($v)) {
$v = strtr($v, ["'" => "\'"]);
- $v = "'$v'";
+ $v = "'{$v}'";
}
return $v;
@@ -523,7 +494,7 @@ public function getConstUses(array $imports): string
*/
protected function getUseType(string $alias, string $name): string
{
- if ($name == $alias || substr($name, -strlen("\\{$alias}")) === "\\{$alias}") {
+ if ($name === $alias || str_ends_with($name, "\\{$alias}")) {
return $name;
}
@@ -543,17 +514,17 @@ public function concat(
string $delimiter,
array $strings,
string $prefix = '',
- string $suffix = ''
+ string $suffix = '',
): string {
$output = implode(
$delimiter,
- array_map(function ($string) use ($delimiter) {
+ array_map(function ($string) use ($delimiter): string {
if (is_string($string)) {
return $string;
}
return implode($delimiter, array_filter($string));
- }, array_filter($strings))
+ }, array_filter($strings)),
);
if ($prefix && !empty($output)) {
@@ -570,8 +541,8 @@ public function concat(
* To be mocked elsewhere...
*
* @param \Cake\ORM\Table $table Table
- * @param string[] $aliases array of aliases
- * @return string[]
+ * @param array $aliases array of aliases
+ * @return array
*/
protected function _filterHasManyAssociationsAliases(Table $table, array $aliases): array
{
diff --git a/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php b/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php
index 456396bc5..014c3d9b4 100644
--- a/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php
+++ b/app/vendor/cakephp/bake/src/View/Helper/DocBlockHelper.php
@@ -5,6 +5,7 @@
use Cake\Collection\Collection;
use Cake\Core\App;
+use Cake\Database\Type\EnumType;
use Cake\Database\TypeFactory;
use Cake\ORM\Association;
use Cake\Utility\Inflector;
@@ -12,13 +13,15 @@
/**
* DocBlock helper
+ *
+ * @extends \Cake\View\Helper<\Cake\View\View>
*/
class DocBlockHelper extends Helper
{
/**
* @var bool Whether to add a blank line between different class annotations
*/
- protected $_annotationSpacing = true;
+ protected bool $_annotationSpacing = true;
/**
* Writes the DocBlock header for a class which includes the property and method declarations. Annotations are
@@ -56,7 +59,7 @@ public function classDescription(string $className, string $classType, array $an
$lines[] = $annotation;
}
- $lines = array_merge(['/**'], (new Collection($lines))->map(function ($line) {
+ $lines = array_merge(['/**'], (new Collection($lines))->map(function ($line): string {
return rtrim(" * {$line}");
})->toArray(), [' */']);
@@ -88,7 +91,7 @@ public function associatedEntityTypeToHintType(string $type, Association $associ
* in generating `@property` hints.
*
* This method expects a property schema as generated by
- * `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`.
+ * `\Bake\Command\ModelCommand::getEntityPropertySchema()`.
*
* The generated map has the format of
*
@@ -99,7 +102,7 @@ public function associatedEntityTypeToHintType(string $type, Association $associ
* ]
* ```
*
- * @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema
+ * @see \Bake\Command\ModelCommand::getEntityPropertySchema
* @param array $propertySchema The property schema to use for generating the type map.
* @return array The property DocType map.
*/
@@ -125,7 +128,7 @@ public function buildEntityPropertyHintTypeMap(array $propertySchema): array
* in generating `@property` hints.
*
* This method expects a property schema as generated by
- * `\Bake\Shell\Task\ModelTask::getEntityPropertySchema()`.
+ * `\Bake\Command\ModelCommand::getEntityPropertySchema()`.
*
* The generated map has the format of
*
@@ -136,7 +139,7 @@ public function buildEntityPropertyHintTypeMap(array $propertySchema): array
* ]
* ```
*
- * @see \Bake\Shell\Task\ModelTask::getEntityPropertySchema
+ * @see \Bake\Command\ModelCommand::getEntityPropertySchema
* @param array $propertySchema The property schema to use for generating the type map.
* @return array The property DocType map.
*/
@@ -150,7 +153,7 @@ public function buildEntityAssociationHintTypeMap(array $propertySchema): array
$properties = $this->_insertAfter(
$properties,
$info['association']->getForeignKey(),
- [$property => $type]
+ [$property => $type],
);
} else {
$properties[$property] = $type;
@@ -201,18 +204,43 @@ public function columnTypeToHintType(string $type): ?string
return 'string|resource';
case 'date':
+ $dbType = TypeFactory::build($type);
+ if (method_exists($dbType, 'getDateClassName')) {
+ // allow custom Date class which should extend \Cake\I18n\Date
+ return '\\' . $dbType->getDateClassName();
+ }
+
+ return '\Cake\I18n\Date';
+
case 'datetime':
case 'datetimefractional':
- case 'time':
case 'timestamp':
case 'timestampfractional':
case 'timestamptimezone':
$dbType = TypeFactory::build($type);
if (method_exists($dbType, 'getDateTimeClassName')) {
+ // allow custom DateTime class which should extend \Cake\I18n\DateTime
return '\\' . $dbType->getDateTimeClassName();
}
+ return '\Cake\I18n\DateTime';
+
+ case 'time':
+ $dbType = TypeFactory::build($type);
+ if (method_exists($dbType, 'getTimeClassName')) {
+ // allow custom Time class which should extend \Cake\I18n\Time
+ return '\\' . $dbType->getTimeClassName();
+ }
+
return '\Cake\I18n\Time';
+
+ default:
+ if (str_starts_with($type, 'enum-')) {
+ $dbType = TypeFactory::build($type);
+ if ($dbType instanceof EnumType) {
+ return '\\' . $dbType->getEnumClassName();
+ }
+ }
}
// Any unique or custom types will have a `string` type hint
@@ -223,8 +251,8 @@ public function columnTypeToHintType(string $type): ?string
* Renders a map of DocBlock property types as an array of
* `@property` hints.
*
- * @param string[] $properties A key value pair where key is the name of a property and the value is the type.
- * @return string[]
+ * @param array $properties A key value pair where key is the name of a property and the value is the type.
+ * @return array
*/
public function propertyHints(array $properties): array
{
@@ -245,14 +273,14 @@ public function propertyHints(array $properties): array
* @param array $behaviors Behaviors list.
* @param string $entity Entity name.
* @param string $namespace Namespace.
- * @return string[]
+ * @return array
*/
public function buildTableAnnotations(
array $associations,
array $associationInfo,
array $behaviors,
string $entity,
- string $namespace
+ string $namespace,
): array {
$annotations = [];
foreach ($associations as $type => $assocs) {
@@ -267,19 +295,19 @@ public function buildTableAnnotations(
// phpcs:disable
$annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} newEmptyEntity()";
$annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} newEntity(array \$data, array \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[] newEntities(array \$data, array \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} get(\$primaryKey, \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} findOrCreate(\$search, ?callable \$callback = null, \$options = [])";
+ $annotations[] = "@method array<\\{$namespace}\\Model\\Entity\\{$entity}> newEntities(array \$data, array \$options = [])";
+ $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} get(mixed \$primaryKey, array|string \$finder = 'all', \\Psr\\SimpleCache\\CacheInterface|string|null \$cache = null, \Closure|string|null \$cacheKey = null, mixed ...\$args)";
+ $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} findOrCreate(\$search, ?callable \$callback = null, array \$options = [])";
$annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} patchEntity(\\Cake\\Datasource\\EntityInterface \$entity, array \$data, array \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[] patchEntities(iterable \$entities, array \$data, array \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}|false save(\\Cake\\Datasource\\EntityInterface \$entity, \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} saveOrFail(\\Cake\\Datasource\\EntityInterface \$entity, \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable \$entities, \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable \$entities, \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable \$entities, \$options = [])";
- $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable \$entities, \$options = [])";
+ $annotations[] = "@method array<\\{$namespace}\\Model\\Entity\\{$entity}> patchEntities(iterable \$entities, array \$data, array \$options = [])";
+ $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity}|false save(\\Cake\\Datasource\\EntityInterface \$entity, array \$options = [])";
+ $annotations[] = "@method \\{$namespace}\\Model\\Entity\\{$entity} saveOrFail(\\Cake\\Datasource\\EntityInterface \$entity, array \$options = [])";
+ $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}>|false saveMany(iterable \$entities, array \$options = [])";
+ $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}> saveManyOrFail(iterable \$entities, array \$options = [])";
+ $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}>|false deleteMany(iterable \$entities, array \$options = [])";
+ $annotations[] = "@method iterable<\\{$namespace}\\Model\\Entity\\{$entity}>|\Cake\Datasource\ResultSetInterface<\\{$namespace}\\Model\\Entity\\{$entity}> deleteManyOrFail(iterable \$entities, array \$options = [])";
// phpcs:enable
- foreach ($behaviors as $behavior => $behaviorData) {
+ foreach (array_keys($behaviors) as $behavior) {
$className = App::className($behavior, 'Model/Behavior', 'Behavior');
if (!$className) {
$className = "Cake\ORM\Behavior\\{$behavior}Behavior";
@@ -301,14 +329,14 @@ public function buildTableAnnotations(
* @param mixed $value The entry to insert.
* @return array The array with the new value inserted.
*/
- protected function _insertAfter(array $target, string $key, $value): array
+ protected function _insertAfter(array $target, string $key, mixed $value): array
{
- $index = array_search($key, array_keys($target));
+ $index = array_search($key, array_keys($target), true);
if ($index !== false) {
$target = array_merge(
array_slice($target, 0, $index + 1),
$value,
- array_slice($target, $index + 1, null)
+ array_slice($target, $index + 1),
);
} else {
$target += (array)$value;
diff --git a/app/vendor/cakephp/bake/templates/bake/Command/command.twig b/app/vendor/cakephp/bake/templates/bake/Command/command.twig
index ab52d71d6..54aec5479 100644
--- a/app/vendor/cakephp/bake/templates/bake/Command/command.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Command/command.twig
@@ -28,18 +28,44 @@
*/
class {{ name }}Command extends Command
{
+ /**
+ * The name of this command.
+ *
+ * @var string
+ */
+ protected string $name = 'cake {{ command_name }}';
+
+ /**
+ * Get the default command name.
+ *
+ * @return string
+ */
+ public static function defaultName(): string
+ {
+ return '{{ command_name }}';
+ }
+
+ /**
+ * Get the command description.
+ *
+ * @return string
+ */
+ public static function getDescription(): string
+ {
+ return 'Command description here.';
+ }
+
/**
* Hook method for defining this command's option parser.
*
- * @see https://book.cakephp.org/4/en/console-commands/commands.html#defining-arguments-and-options
+ * @link https://book.cakephp.org/5/en/console-commands/commands.html#defining-arguments-and-options
* @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined
* @return \Cake\Console\ConsoleOptionParser The built parser.
*/
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
- $parser = parent::buildOptionParser($parser);
-
- return $parser;
+ return parent::buildOptionParser($parser)
+ ->setDescription(static::getDescription());
}
/**
@@ -47,7 +73,7 @@ class {{ name }}Command extends Command
*
* @param \Cake\Console\Arguments $args The command arguments.
* @param \Cake\Console\ConsoleIo $io The console io
- * @return null|void|int The exit code or null for success
+ * @return int|null|void The exit code or null for success
*/
public function execute(Arguments $args, ConsoleIo $io)
{
diff --git a/app/vendor/cakephp/bake/templates/bake/Controller/component.twig b/app/vendor/cakephp/bake/templates/bake/Controller/component.twig
index 8f4b42bbe..f7861b8da 100644
--- a/app/vendor/cakephp/bake/templates/bake/Controller/component.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Controller/component.twig
@@ -31,5 +31,5 @@ class {{ name }}Component extends Component
*
* @var array
*/
- protected $_defaultConfig = [];
+ protected array $_defaultConfig = [];
}
diff --git a/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig b/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig
index 894c2cda9..3212114f6 100644
--- a/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Controller/controller.twig
@@ -21,6 +21,8 @@
namespace: "#{namespace}\\Controller#{prefix}",
classImports: (plugin or prefix) ? ["#{baseNamespace}\\Controller\\AppController"] : [],
}) }}
+{% set has_login = 'login' in actions and Bake.hasPlugin('Authentication') %}
+{% set include_initialize = components or helpers or has_login %}
/**
* {{ name }} Controller
@@ -30,17 +32,13 @@
{% endif %}
{%- for component in components %}
-{% set classInfo = Bake.classInfo(component, 'Controller/Component', 'Component') %}
+ {%~ set classInfo = Bake.classInfo(component, 'Controller/Component', 'Component') %}
* @property {{ classInfo.fqn }} ${{ classInfo.name }}
{% endfor %}
-
-{%- if 'index' in actions %}
- * @method \{{ namespace }}\Model\Entity\{{ entityClassName }}[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
-{% endif %}
*/
class {{ name }}Controller extends AppController
{
-{% if components or helpers %}
+{% if include_initialize %}
/**
* Initialize controller
*
@@ -50,17 +48,20 @@ class {{ name }}Controller extends AppController
{
parent::initialize();
-{% for component in components %}
+ {%~ for component in components %}
$this->loadComponent('{{ component }}');
-{% endfor %}
-{% if helpers %}
+ {%~ endfor %}
+ {%~ if helpers %}
$this->viewBuilder()->setHelpers({{ Bake.exportArray(helpers)|raw }});
-{% endif %}
+ {%~ endif %}
+ {%~ if has_login %}
+ $this->Authentication->allowUnauthenticated(['login']);
+ {%~ endif %}
}
-{% if actions|length %}{{ "\n" }}{% endif %}
+ {%~ if actions|length %}{{ "\n" }}{% endif %}
{% endif %}
-{%- for action in actions %}
-{% if loop.index > 1 %}{{ "\n" }}{% endif %}
+{% for action in actions %}
+ {%~ if loop.index > 1 %}{{ "\n" }}{% endif %}
{{- element('Bake.Controller/' ~ action) -}}
{% endfor %}
}
diff --git a/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig b/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig
index 55c01a532..88a442d05 100644
--- a/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Mailer/mailer.twig
@@ -30,5 +30,5 @@ class {{ name }}Mailer extends Mailer
*
* @var string
*/
- public static $name = '{{ name }}';
+ public static string $name = '{{ name }}';
}
diff --git a/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig b/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig
index 5a40c4a69..c314e7f96 100644
--- a/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Model/behavior.twig
@@ -31,5 +31,5 @@ class {{ name }}Behavior extends Behavior
*
* @var array
*/
- protected $_defaultConfig = [];
+ protected array $_defaultConfig = [];
}
diff --git a/app/vendor/cakephp/bake/templates/bake/Model/entity.twig b/app/vendor/cakephp/bake/templates/bake/Model/entity.twig
index 68d599731..ab924dd7b 100644
--- a/app/vendor/cakephp/bake/templates/bake/Model/entity.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Model/entity.twig
@@ -18,8 +18,8 @@
{% set annotations = DocBlock.propertyHints(propertyHintMap) %}
{%- if associationHintMap %}
- {%- set annotations = annotations|merge(['']) %}
- {%- set annotations = annotations|merge(DocBlock.propertyHints(associationHintMap)) %}
+ {%~ set annotations = annotations|merge(['']) %}
+ {%~ set annotations = annotations|merge(DocBlock.propertyHints(associationHintMap)) %}
{% endif %}
{%- set accessible = Bake.getFieldAccessibility(fields, primaryKey) %}
@@ -39,7 +39,7 @@ class {{ name }} extends Entity{{ fileBuilder.classBuilder.implements ? ' implem
{% endif %}
{% if accessible %}
-{%- set generatedProperties = generatedProperties|merge(['_accessible']) %}
+{%~ set generatedProperties = generatedProperties|merge(['_accessible']) %}
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
@@ -49,19 +49,19 @@ class {{ name }} extends Entity{{ fileBuilder.classBuilder.implements ? ' implem
*
* @var array
*/
- protected $_accessible = {{ Bake.exportVar(accessible, 1)|raw }};
+ protected array $_accessible = {{ Bake.exportVar(accessible, 1)|raw }};
{% endif %}
{% if accessible and hidden %}
{% endif %}
-{%- if hidden %}
-{%- set generatedProperties = generatedProperties|merge(['_hidden']) %}
+{% if hidden %}
+ {%~ set generatedProperties = generatedProperties|merge(['_hidden']) %}
/**
* Fields that are excluded from JSON versions of the entity.
*
* @var array
*/
- protected $_hidden = {{ Bake.exportVar(hidden, 1)|raw }};
+ protected array $_hidden = {{ Bake.exportVar(hidden, 1)|raw }};
{% endif %}
{% set userProperties = fileBuilder.classBuilder.userProperties(generatedProperties) %}
{% if userProperties %}
diff --git a/app/vendor/cakephp/bake/templates/bake/Model/enum.twig b/app/vendor/cakephp/bake/templates/bake/Model/enum.twig
new file mode 100644
index 000000000..83f48243a
--- /dev/null
+++ b/app/vendor/cakephp/bake/templates/bake/Model/enum.twig
@@ -0,0 +1,38 @@
+{#
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link https://cakephp.org CakePHP(tm) Project
+ * @since 3.1.0
+ * @license https://www.opensource.org/licenses/mit-license.php MIT License
+ */
+#}
+{{ element('Bake.file_header', {
+ namespace: "#{namespace}\\Model\\Enum",
+ classImports: [
+ 'Cake\\Database\\Type\\EnumLabelInterface',
+ 'Cake\\Utility\\Inflector',
+ ],
+}) }}
+
+{{ DocBlock.classDescription(name, 'Enum', [])|raw }}
+enum {{ name }}: {{ backingType }} implements EnumLabelInterface
+{
+{% if cases %}
+ {{ Bake.concat('\n ', cases) }}
+
+{% endif %}
+ /**
+ * @return string
+ */
+ public function label(): string
+ {
+ return Inflector::humanize(Inflector::underscore($this->name));
+ }
+}
diff --git a/app/vendor/cakephp/bake/templates/bake/Model/table.twig b/app/vendor/cakephp/bake/templates/bake/Model/table.twig
index 7c213e0dc..76511babc 100644
--- a/app/vendor/cakephp/bake/templates/bake/Model/table.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Model/table.twig
@@ -17,7 +17,7 @@
{% set generatedFunctions = ['initialize'] %}
{{ element('Bake.file_header', {
namespace: fileBuilder.namespace,
- classImports: fileBuilder.classImports(['Cake\\ORM\\Query', 'Cake\\ORM\\RulesChecker', 'Cake\\ORM\\Table', 'Cake\\Validation\\Validator']),
+ classImports: fileBuilder.classImports(['Cake\\ORM\\Query\\SelectQuery', 'Cake\\ORM\\RulesChecker', 'Cake\\ORM\\Table', 'Cake\\Validation\\Validator']),
}) }}
{{ DocBlock.classDescription(name, 'Model', annotations)|raw }}
@@ -36,7 +36,7 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im
/**
* Initialize method
*
- * @param array $config The configuration for the Table.
+ * @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config): void
@@ -50,47 +50,49 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im
{%- if displayField %}
$this->setDisplayField({{ (displayField is iterable ? Bake.exportArray(displayField) : Bake.exportVar(displayField))|raw }});
{% endif %}
-
-{%- if primaryKey %}
- {%- if primaryKey is iterable and primaryKey|length > 1 %}
+{% if primaryKey %}
+ {%~ if primaryKey is iterable and primaryKey|length > 1 %}
$this->setPrimaryKey({{ Bake.exportArray(primaryKey)|raw }});
- {{- "\n" }}
- {%- else %}
+ {%~ else %}
$this->setPrimaryKey('{{ primaryKey|as_array|first }}');
- {{- "\n" }}
- {%- endif %}
+ {%~ endif %}
{% endif %}
+{% if enums %}
-{%- if behaviors %}
+ {%~ for name, className in enums %}
+ $this->getSchema()->setColumnType('{{ name }}', \Cake\Database\Type\EnumType::from(\{{ className }}::class));
+ {%~ endfor %}
+{% endif %}
+{% if customColumnTypes is defined and customColumnTypes %}
+ {%~ for columnName, typeExpression in customColumnTypes %}
+ $this->getSchema()->setColumnType('{{ columnName }}', {{ typeExpression|raw }});
+ {%~ endfor %}
{% endif %}
+{% if behaviors %}
-{%- for behavior, behaviorData in behaviors %}
+ {%~ for behavior, behaviorData in behaviors %}
$this->addBehavior('{{ behavior }}'{{ (behaviorData ? (", " ~ Bake.exportArray(behaviorData, 2)|raw ~ '') : '')|raw }});
-{% endfor %}
-
-{%- if associations.belongsTo or associations.hasMany or associations.belongsToMany %}
-
+ {%~ endfor %}
{% endif %}
-
-{%- for type, assocs in associations %}
- {%- for assoc in assocs %}
- {%- set assocData = [] %}
- {%- for key, val in assoc %}
- {%- if key is not same as('alias') %}
- {%- set assocData = assocData|merge({(key): val}) %}
- {%- endif %}
- {%- endfor %}
+{% if associations.belongsTo or associations.hasMany or associations.belongsToMany %}
+
+ {%~ for type, assocs in associations %}
+ {%~ for assoc in assocs %}
+ {%~ set assocData = [] %}
+ {%~ for key, val in assoc %}
+ {%~ if key is not same as('alias') %}
+ {%~ set assocData = assocData|merge({(key): val}) %}
+ {%~ endif %}
+ {%~ endfor %}
$this->{{ type }}('{{ assoc.alias }}', {{ Bake.exportArray(assocData, 2)|raw }});
- {{- "\n" }}
- {%- endfor %}
-{% endfor %}
+ {%~ endfor %}
+ {%~ endfor %}
+{% endif %}
}
-{{- "\n" }}
-
-{%- if validation %}
-{% set generatedFunctions = generatedFunctions|merge(['validationDefault']) %}
+{% if validation %}
+ {%~ set generatedFunctions = generatedFunctions|merge(['validationDefault']) %}
/**
* Default validation rules.
*
@@ -99,25 +101,24 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im
*/
public function validationDefault(Validator $validator): Validator
{
-{% for field, rules in validation %}
-{% set validationMethods = Bake.getValidationMethods(field, rules) %}
-{% if validationMethods %}
+ {%~ for field, rules in validation %}
+ {%~ set validationMethods = Bake.getValidationMethods(field, rules) %}
+ {%~ if validationMethods %}
$validator
-{% for validationMethod in validationMethods %}
-{% if loop.last %}
-{% set validationMethod = validationMethod ~ ';' %}
-{% endif %}
+ {%~ for validationMethod in validationMethods %}
+ {%~ if loop.last %}
+ {%~ set validationMethod = validationMethod ~ ';' %}
+ {%~ endif %}
{{ validationMethod|raw }}
-{% endfor %}
+ {%~ endfor %}
-{% endif %}
-{% endfor %}
+ {%~ endif %}
+ {%~ endfor %}
return $validator;
}
{% endif %}
-
{%- if rulesChecker %}
-{% set generatedFunctions = generatedFunctions|merge(['buildRules']) %}
+ {%~ set generatedFunctions = generatedFunctions|merge(['buildRules']) %}
/**
* Returns a rules checker object that will be used for validating
@@ -128,21 +129,24 @@ class {{ name }}Table extends Table{{ fileBuilder.classBuilder.implements ? ' im
*/
public function buildRules(RulesChecker $rules): RulesChecker
{
-{% for field, rule in rulesChecker %}
-{% set fields = rule.fields is defined ? Bake.exportArray(rule.fields) : Bake.exportVar(field) %}
-{% set options = '' %}
-{% for optionName, optionValue in rule.options %}
- {%~ set options = (loop.first ? '[' : options) ~ "'#{optionName}' => " ~ Bake.exportVar(optionValue) ~ (loop.last ? ']' : ', ') %}
-{% endfor %}
- $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), ['errorField' => '{{ field }}']);
-{% endfor %}
+ {%~ for rule in rulesChecker %}
+ {%~ set fields = Bake.exportArray(rule.fields) %}
+ {%~ set options = '' %}
+ {%~ for optionName, optionValue in rule.options %}
+ {%~ set options = (loop.first ? '[' : options) ~ "'#{optionName}' => " ~ Bake.exportVar(optionValue) ~ (loop.last ? ']' : ', ') %}
+ {%~ endfor %}
+ {%~ set ruleOptions = "'errorField' => '" ~ rule.fields[0] ~ "'" %}
+ {%~ if rule.message is defined %}
+ {%~ set ruleOptions = ruleOptions ~ ", 'message' => __(" ~ Bake.exportVar(rule.message) ~ ")" %}
+ {%~ endif %}
+ $rules->add($rules->{{ rule.name }}({{ fields|raw }}{{ (rule.extra|default ? ", '#{rule.extra}'" : '')|raw }}{{ (options ? ', ' ~ options : '')|raw }}), [{{ ruleOptions|raw }}]);
+ {%~ endfor %}
return $rules;
}
{% endif %}
-
-{%- if connection is not same as('default') %}
-{% set generatedFunctions = generatedFunctions|merge(['defaultConnectionName']) %}
+{% if connection is not same as('default') %}
+ {%~ set generatedFunctions = generatedFunctions|merge(['defaultConnectionName']) %}
/**
* Returns the database connection name to use by default.
diff --git a/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig b/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig
index 75e0699f3..a4f95ba7f 100644
--- a/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Plugin/composer.json.twig
@@ -20,11 +20,11 @@
"type": "cakephp-plugin",
"license": "MIT",
"require": {
- "php": ">=7.2",
+ "php": ">=8.1",
"cakephp/cakephp": "{{ cakeVersion|raw }}"
},
"require-dev": {
- "phpunit/phpunit": "^8.5 || ^9.3"
+ "phpunit/phpunit": "^10.1"
},
"autoload": {
"psr-4": {
diff --git a/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig b/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig
index 7a6f6e717..ae6b5c53f 100644
--- a/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Plugin/phpunit.xml.dist.twig
@@ -34,12 +34,12 @@
-
+
-
-
+
+
src/
-
-
+
+
diff --git a/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig b/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig
index a7d1575ac..e1982d0f2 100644
--- a/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Plugin/src/Plugin.php.twig
@@ -41,6 +41,7 @@ class {{ name }}Plugin extends BasePlugin
*/
public function bootstrap(PluginApplicationInterface $app): void
{
+ // remove this method hook if you don't need it
}
/**
@@ -54,6 +55,7 @@ class {{ name }}Plugin extends BasePlugin
*/
public function routes(RouteBuilder $routes): void
{
+ // remove this method hook if you don't need it
$routes->plugin(
'{{ plugin }}',
['path' => '/{{ routePath }}'],
@@ -75,6 +77,7 @@ class {{ name }}Plugin extends BasePlugin
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
// Add your middlewares here
+ // remove this method hook if you don't need it
return $middlewareQueue;
}
@@ -88,6 +91,7 @@ class {{ name }}Plugin extends BasePlugin
public function console(CommandCollection $commands): CommandCollection
{
// Add your commands here
+ // remove this method hook if you don't need it
$commands = parent::console($commands);
@@ -99,10 +103,11 @@ class {{ name }}Plugin extends BasePlugin
*
* @param \Cake\Core\ContainerInterface $container The Container to update.
* @return void
- * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
+ * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection
*/
public function services(ContainerInterface $container): void
{
// Add your services here
+ // remove this method hook if you don't need it
}
}
diff --git a/app/vendor/cakephp/bake/templates/bake/Shell/helper.twig b/app/vendor/cakephp/bake/templates/bake/Shell/helper.twig
deleted file mode 100644
index 799299feb..000000000
--- a/app/vendor/cakephp/bake/templates/bake/Shell/helper.twig
+++ /dev/null
@@ -1,39 +0,0 @@
-{#
-/**
- * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
- * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- *
- * Licensed under The MIT License
- * For full copyright and license information, please see the LICENSE.txt
- * Redistributions of files must retain the above copyright notice.
- *
- * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
- * @link https://cakephp.org CakePHP(tm) Project
- * @since 2.0.0
- * @license https://www.opensource.org/licenses/mit-license.php MIT License
- */
-#}
-{{ element('Bake.file_header', {
- namespace: "#{namespace}\\Shell\\Helper",
- classImports: [
- 'Cake\Console\Helper',
- ],
-}) }}
-
-/**
- * {{ name }} shell helper.
- */
-class {{ name }}Helper extends Helper
-{
- /**
- * Output method.
- *
- * Generate the output for this shell helper.
- *
- * @param array $args Arguments.
- * @return void
- */
- public function output(array $args): void
- {
- }
-}
diff --git a/app/vendor/cakephp/bake/templates/bake/Template/add.twig b/app/vendor/cakephp/bake/templates/bake/Template/add.twig
index c21d7c1d2..bfbfcbada 100644
--- a/app/vendor/cakephp/bake/templates/bake/Template/add.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Template/add.twig
@@ -17,18 +17,15 @@
/**
* @var \{{ namespace }}\View\AppView $this
* @var \{{ entityClass }} ${{ singularVar }}
- {{- "\n" }}
-{%- if associations.BelongsTo is defined %}
- {%- for assocName, assocData in associations.BelongsTo %}
+{% if associations.BelongsTo is defined %}
+ {%~ for assocName, assocData in associations.BelongsTo %}
* @var \Cake\Collection\CollectionInterface|string[] ${{ assocData.variable }}
- {{- "\n" }}
- {%- endfor %}
+ {%~ endfor %}
{% endif %}
-{%- if associations.BelongsToMany is defined %}
- {%- for assocName, assocData in associations.BelongsToMany %}
+{% if associations.BelongsToMany is defined %}
+ {%~ for assocName, assocData in associations.BelongsToMany %}
* @var \Cake\Collection\CollectionInterface|string[] ${{ assocData.variable }}
- {{- "\n" }}
- {%- endfor %}
+ {%~ endfor %}
{% endif %}
*/
?>
diff --git a/app/vendor/cakephp/bake/templates/bake/Template/edit.twig b/app/vendor/cakephp/bake/templates/bake/Template/edit.twig
index 90fed4ec4..a7d57e8ff 100644
--- a/app/vendor/cakephp/bake/templates/bake/Template/edit.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Template/edit.twig
@@ -17,18 +17,15 @@
/**
* @var \{{ namespace }}\View\AppView $this
* @var \{{ entityClass }} ${{ singularVar }}
- {{- "\n" }}
-{%- if associations.BelongsTo is defined %}
- {%- for assocName, assocData in associations.BelongsTo %}
+{% if associations.BelongsTo is defined %}
+ {%~ for assocName, assocData in associations.BelongsTo %}
* @var string[]|\Cake\Collection\CollectionInterface ${{ assocData.variable }}
- {{- "\n" }}
- {%- endfor %}
+ {%~ endfor %}
{% endif %}
-{%- if associations.BelongsToMany is defined %}
- {%- for assocName, assocData in associations.BelongsToMany %}
+{% if associations.BelongsToMany is defined %}
+ {%~ for assocName, assocData in associations.BelongsToMany %}
* @var string[]|\Cake\Collection\CollectionInterface ${{ assocData.variable }}
- {{- "\n" }}
- {%- endfor %}
+ {%~ endfor %}
{% endif %}
*/
?>
diff --git a/app/vendor/cakephp/bake/templates/bake/Template/index.twig b/app/vendor/cakephp/bake/templates/bake/Template/index.twig
index 5748cbb83..4a81b76fe 100644
--- a/app/vendor/cakephp/bake/templates/bake/Template/index.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Template/index.twig
@@ -38,31 +38,41 @@
{% 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'] %}
+ {%~ set isKey = false %}
+ {%~ if associations.BelongsTo is defined %}
+ {%~ for alias, details in associations.BelongsTo %}
+ {%~ if field == details.foreignKey %}
+ {%~ set isKey = true %}
+ = ${{ singularVar }}->hasValue('{{ 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) %}
+ {%~ set supportsLabel = Bake.enumSupportsLabel(field, schema) %}
+ {%~ if columnData.type starts with 'enum-' %}
+ = ${{ singularVar }}->{{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->{% if supportsLabel %}label(){% else %}value{% endif %}) ?>
+ {%~ elseif columnData.type not in ['integer', 'float', 'decimal', 'biginteger', 'smallinteger', 'tinyinteger'] %}
= h(${{ singularVar }}->{{ field }}) ?>
-{% elseif columnData.null %}
+ {%~ elseif columnData.null %}
= ${{ singularVar }}->{{ field }} === null ? '' : $this->Number->format(${{ singularVar }}->{{ field }}) ?>
-{% else %}
+ {%~ else %}
= $this->Number->format(${{ singularVar }}->{{ field }}) ?>
-{% endif %}
-{% endif %}
+ {%~ 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->Form->postLink(
+ __('Delete'),
+ ['action' => 'delete', {{ pk|raw }}],
+ [
+ 'method' => 'delete',
+ 'confirm' => __('Are you sure you want to delete # {0}?', {{ pk|raw }}),
+ ]
+ ) ?>
@@ -79,4 +89,4 @@
= $this->Paginator->counter(__('Page {{ '{{' }}page{{ '}}' }} of {{ '{{' }}pages{{ '}}' }}, showing {{ '{{' }}current{{ '}}' }} record(s) out of {{ '{{' }}count{{ '}}' }} total')) ?>
-
+
\ No newline at end of file
diff --git a/app/vendor/cakephp/bake/templates/bake/Template/view.twig b/app/vendor/cakephp/bake/templates/bake/Template/view.twig
index 0e75b8a41..a6841f352 100644
--- a/app/vendor/cakephp/bake/templates/bake/Template/view.twig
+++ b/app/vendor/cakephp/bake/templates/bake/Template/view.twig
@@ -24,6 +24,7 @@
{% set associationFields = fieldsData.associationFields %}
{% set groupedFields = fieldsData.groupedFields %}
{% set pK = '$' ~ singularVar ~ '->' ~ primaryKey[0] %}
+{% set done = [] %}
@@ -32,102 +33,125 @@
= $this->Form->postLink(__('Delete {{ singularHumanName }}'), ['action' => 'delete', {{ pK|raw }}], ['confirm' => __('Are you sure you want to delete # {0}?', {{ pK|raw }}), 'class' => 'side-nav-item']) ?>
= $this->Html->link(__('List {{ pluralHumanName }}'), ['action' => 'index'], ['class' => 'side-nav-item']) ?>
= $this->Html->link(__('New {{ singularHumanName }}'), ['action' => 'add'], ['class' => 'side-nav-item']) ?>
-{% set done = [] %}
-
+
= h(${{ singularVar }}->{{ displayField }}) ?>
{% if groupedFields['string'] %}
-{% for field in groupedFields['string'] %}
-{% if associationFields[field] is defined %}
-{% set details = associationFields[field] %}
+ {%~ for field in groupedFields['string'] %}
+ {%~ if associationFields[field] is defined %}
+ {%~ set details = associationFields[field] %}
= __('{{ 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] }}]) : '' ?>
+ = ${{ singularVar }}->hasValue('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
-{% else %}
+ {%~ else %}
= __('{{ field|humanize }}') ?>
= h(${{ singularVar }}->{{ field }}) ?>
-{% endif %}
-{% endfor %}
+ {%~ endif %}
+ {%~ endfor %}
{% endif %}
{% if associations.HasOne %}
-{% for alias, details in associations.HasOne %}
+ {%~ for alias, details in associations.HasOne %}
= __('{{ 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] }}]) : '' ?>
+ = ${{ singularVar }}->hasValue('{{ details.property }}') ? $this->Html->link(${{ singularVar }}->{{ details.property }}->{{ details.displayField }}, ['controller' => '{{ details.controller }}', 'action' => 'view', ${{ singularVar }}->{{ details.property }}->{{ details.primaryKey[0] }}]) : '' ?>
-{% endfor %}
+ {%~ endfor %}
{% endif %}
{% if groupedFields.number %}
-{% for field in groupedFields.number %}
+ {%~ for field in groupedFields.number %}
= __('{{ field|humanize }}') ?>
-{% set columnData = Bake.columnData(field, schema) %}
-{% if columnData.null %}
+ {%~ set columnData = Bake.columnData(field, schema) %}
+ {%~ if columnData.null %}
= ${{ singularVar }}->{{ field }} === null ? '' : $this->Number->format(${{ singularVar }}->{{ field }}) ?>
-{% else %}
+ {%~ else %}
= $this->Number->format(${{ singularVar }}->{{ field }}) ?>
+ {%~ endif %}
+
+ {%~ endfor %}
{% endif %}
+{% if groupedFields.enum %}
+ {%~ for field in groupedFields.enum %}
+
+ = __('{{ field|humanize }}') ?>
+ {%~ set columnData = Bake.columnData(field, schema) %}
+ {%~ set supportsLabel = Bake.enumSupportsLabel(field, schema) %}
+ {%~ if columnData.null %}
+ = ${{ singularVar }}->{{ field }} === null ? '' : h(${{ singularVar }}->{{ field }}->{% if supportsLabel %}label(){% else %}value{% endif %}) ?>
+ {%~ else %}
+ = h(${{ singularVar }}->{{ field }}->{% if supportsLabel %}label(){% else %}value{% endif %}) ?>
+ {%~ endif %}
-{% endfor %}
+ {%~ endfor %}
{% endif %}
{% if groupedFields.date %}
-{% for field in groupedFields.date %}
+ {%~ for field in groupedFields.date %}
= __('{{ field|humanize }}') ?>
= h(${{ singularVar }}->{{ field }}) ?>
-{% endfor %}
+ {%~ endfor %}
{% endif %}
{% if groupedFields.boolean %}
-{% for field in groupedFields.boolean %}
+ {%~ for field in groupedFields.boolean %}
= __('{{ field|humanize }}') ?>
= ${{ singularVar }}->{{ field }} ? __('Yes') : __('No'); ?>
-{% endfor %}
+ {%~ endfor %}
{% endif %}
{% if groupedFields.text %}
-{% for field in groupedFields.text %}
+ {%~ for field in groupedFields.text %}
= __('{{ field|humanize }}') ?>
= $this->Text->autoParagraph(h(${{ singularVar }}->{{ field }})); ?>
-{% endfor %}
+ {%~ endfor %}
{% endif %}
{% set relations = associations.BelongsToMany|merge(associations.HasMany) %}
{% for alias, details in relations %}
-{% set otherSingularVar = alias|variable %}
-{% set otherPluralHumanName = details.controller|underscore|humanize %}
+ {%~ set otherSingularVar = alias|singularize|variable %}
+ {%~ if otherSingularVar == singularVar %}
+ {%~ set otherSingularVar = otherSingularVar ~ 'Related' %}
+ {%~ endif %}
+ {%~ set otherPluralHumanName = details.controller|underscore|humanize %}