From e98eaa717f93e6414d598ef8050d227e26d858f0 Mon Sep 17 00:00:00 2001 From: Benn Oshrin Date: Thu, 13 Feb 2025 20:39:09 -0500 Subject: [PATCH] Fix incomplete commit 4ab577953b --- NOTICE | 3 +- app/composer.json | 2 + app/config/schema/schema.json | 24 +- .../resources/locales/en_US/core_enroller.po | 24 ++ .../Controller/EmailVerifiersController.php | 3 +- .../IdentifierCollectorsController.php | 1 + .../src/Model/Entity/AttributeCollector.php | 2 + .../Model/Entity/BasicAttributeCollector.php | 2 + .../src/Model/Entity/EmailVerifier.php | 2 + .../src/Model/Entity/IdentifierCollector.php | 2 + .../src/Model/Entity/InvitationAccepter.php | 2 + .../Table/BasicAttributeCollectorsTable.php | 14 +- .../src/Model/Table/EmailVerifiersTable.php | 16 +- .../Model/Table/IdentifierCollectorsTable.php | 8 +- .../Model/Table/InvitationAcceptersTable.php | 8 +- .../Table/PetitionVerificationsTable.php | 4 - app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php | 19 +- app/resources/locales/en_US/command.po | 15 ++ app/resources/locales/en_US/controller.po | 3 + app/resources/locales/en_US/enumeration.po | 3 + app/resources/locales/en_US/error.po | 18 ++ app/resources/locales/en_US/field.po | 27 ++ app/resources/locales/en_US/information.po | 15 ++ app/resources/locales/en_US/result.po | 32 ++- app/src/Command/SetupCommand.php | 4 + app/src/Command/TransmogrifyCommand.php | 3 +- app/src/Controller/AppController.php | 13 +- .../Component/RegistryAuthComponent.php | 2 +- app/src/Controller/DashboardsController.php | 9 +- .../Controller/EmailAddressesController.php | 4 +- .../Controller/EnrollmentFlowsController.php | 36 ++- app/src/Controller/PetitionsController.php | 50 ++-- app/src/Controller/StandardController.php | 11 +- .../Controller/StandardEnrollerController.php | 4 +- .../Controller/StandardPluginController.php | 14 +- app/src/Controller/TrafficController.php | 108 ++++++-- app/src/Lib/Enum/ActionEnum.php | 5 +- app/src/Lib/Enum/PetitionActionEnum.php | 1 + app/src/Lib/Events/ChangelogEventListener.php | 87 ------- app/src/Lib/Events/CoIdEventListener.php | 16 +- .../Lib/Traits/EnrollmentControllerTrait.php | 19 +- app/src/Lib/Traits/EntityMetaTrait.php | 15 ++ app/src/Lib/Traits/PluggableModelTrait.php | 48 +++- app/src/Lib/Traits/PrimaryLinkTrait.php | 5 +- app/src/Lib/Traits/TableMetaTrait.php | 125 +++++++++ app/src/Lib/Traits/ValidationTrait.php | 28 ++- app/src/Lib/Util/SchemaManager.php | 38 ++- app/src/Model/Behavior/OrderableBehavior.php | 33 +-- app/src/Model/Entity/EnrollmentFlow.php | 1 + app/src/Model/Entity/EnrollmentFlowStep.php | 1 + app/src/Model/Entity/MostlyStaticPage.php | 7 +- app/src/Model/Table/EmailAddressesTable.php | 17 +- app/src/Model/Table/EnrollmentFlowsTable.php | 19 +- .../Table/ExtIdentitySourceRecordsTable.php | 2 +- .../Table/ExternalIdentitySourcesTable.php | 17 +- app/src/Model/Table/HistoryRecordsTable.php | 36 ++- app/src/Model/Table/MetaTable.php | 31 ++- .../Model/Table/MostlyStaticPagesTable.php | 21 +- .../Table/PetitionHistoryRecordsTable.php | 26 +- app/src/Model/Table/PetitionsTable.php | 238 ++++++++++++++---- app/src/Model/Table/PipelinesTable.php | 187 ++++++++++---- app/src/Model/Table/VerificationsTable.php | 186 ++++++++++++-- app/templates/EnrollmentFlows/columns.inc | 5 + app/templates/EnrollmentFlows/fields.inc | 1 + .../ExtIdentitySourceRecords/fields.inc | 29 ++- app/templates/HistoryRecords/fields.inc | 4 +- app/templates/Petitions/columns.inc | 3 + app/templates/Pipelines/fields.inc | 1 + app/vendor/cakephp-plugins.php | 1 + app/vendor/composer/autoload_psr4.php | 2 + app/vendor/composer/autoload_static.php | 13 + 71 files changed, 1381 insertions(+), 394 deletions(-) delete mode 100644 app/src/Lib/Events/ChangelogEventListener.php diff --git a/NOTICE b/NOTICE index 962e0147b..96f6d7083 100644 --- a/NOTICE +++ b/NOTICE @@ -1,7 +1,8 @@ COmanage Registry -Copyright (C) 2010-2024 +Copyright (C) 2010-2025 University Corporation for Advanced Internet Development, Inc. +SCG Collaboration Group Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License. diff --git a/app/composer.json b/app/composer.json index d9672ff81..9aa96ddbf 100644 --- a/app/composer.json +++ b/app/composer.json @@ -35,6 +35,7 @@ "CoreAssigner\\": "plugins/CoreAssigner/src/", "CoreEnroller\\": "plugins/CoreEnroller/src/", "CoreServer\\": "plugins/CoreServer/src/", + "EnvSource\\": "plugins/EnvSource/src/", "FileConnector\\": "availableplugins/FileConnector/src/", "PipelineToolkit\\": "availableplugins/PipelineToolkit/src/", "SqlConnector\\": "availableplugins/SqlConnector/src/", @@ -49,6 +50,7 @@ "CoreAssigner\\Test\\": "plugins/CoreAssigner/tests/", "CoreEnroller\\Test\\": "plugins/CoreEnroller/tests/", "CoreServer\\Test\\": "plugins/CoreServer/tests/", + "EnvSource\\Test\\": "plugins/EnvSource/tests/", "FileConnector\\Test\\": "availableplugins/FileConnector/tests/", "PipelineToolkit\\Test\\": "availableplugins/PipelineToolkit/tests/", "SqlConnector\\Test\\": "availableplugins/SqlConnector/tests/", diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index 240687bdd..28b763bf9 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -39,6 +39,7 @@ "server_id": { "type": "integer", "foreignkey": { "table": "servers", "column": "id" }, "notnull": true }, "sor_label": { "type": "string", "size": 40 }, "status": { "type": "string", "size": 2 }, + "traffic_detour_id": { "type": "integer", "foreignkey": { "table": "traffic_detours", "column": "id" }, "notnull": true }, "type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" }, "notnull": true }, "valid_from": { "type": "datetime" }, "valid_through": { "type": "datetime" } @@ -90,6 +91,7 @@ "display_name": { "type": "string", "size": 64, "notnull": true }, "value": { "type": "string", "size": 32, "notnull": true }, "edupersonaffiliation": { "type": "string", "size": 32 }, + "case_insensitive": { "type": "boolean", "XXX": "CFM-15" }, "status": {} }, "indexes": { @@ -509,7 +511,7 @@ "urls": { "columns": { "id": {}, - "url": { "type": "string", "size": 256 }, + "url": { "type": "url" }, "description": {}, "type_id": {} }, @@ -628,6 +630,7 @@ "authz_cou_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" }}, "authz_group_id": { "type": "integer", "foreignkey": { "table": "groups", "column": "id" }}, "collect_enrollee_email": { "type": "boolean" }, + "redirect_on_duplicate": { "type": "string", "size": 256 }, "redirect_on_finalize": { "type": "string", "size": 256 } }, "indexes": { @@ -712,6 +715,7 @@ "verification_time": { "type": "datetime" }, "request_expiration_time": { "type": "datetime" }, "method": { "type": "string", "size": 2 }, + "trusted_source": { "type": "string", "size": 128 }, "email_address_id": { "type": "integer", "foreignkey": { "table": "email_addresses", "column": "id" } }, "petition_id": {} }, @@ -806,7 +810,8 @@ "sync_affiliation_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, "sync_cou_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } }, "sync_replace_cou_id": { "type": "integer", "foreignkey": { "table": "cous", "column": "id" } }, - "sync_identifier_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } } + "sync_identifier_type_id": { "type": "integer", "foreignkey": { "table": "types", "column": "id" } }, + "sync_verify_email_addresses": { "type": "boolean" } }, "indexes": { "pipelines_i1": { "columns": [ "co_id" ] }, @@ -835,6 +840,9 @@ }, "external_identity_sources": { + "comments": [ + "As a general rule, configurations that modify source data belong in the Pipeline" + ], "columns": { "id": {}, "co_id": {}, @@ -884,6 +892,18 @@ "application_states_i1": { "columns": [ "co_id" ] }, "application_states_i2": { "columns": [ "person_id" ] } } + }, + + "traffic_detours": { + "columns": { + "id": {}, + "description": {}, + "plugin": {}, + "status": {}, + "ordr": {} + }, + "indexes": { + } } }, diff --git a/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po b/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po index 20db7a858..5a41e73cb 100644 --- a/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po +++ b/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po @@ -28,9 +28,33 @@ msgstr "{0,plural,=1{Attribute Collector} other{Attribute Collectors}}" msgid "controller.BasicAttributeCollectors" msgstr "{0,plural,=1{Basic Attribute Collector} other{Basic Attribute Collectors}}" +msgid "controller.EmailVerifiers" +msgstr "{0,plural,=1{Email Verifier} other{Email Verifiers}}" + msgid "controller.EnrollmentAttributes" msgstr "{0,plural,=1{Enrollment Attribute} other{Enrollment Attributes}}" +msgid "controller.IdentifierCollectors" +msgstr "{0,plural,=1{Identifier Collector} other{Identifier Collectors}}" + +msgid "controller.InvitationAccepters" +msgstr "{0,plural,=1{Invitation Accepter} other{Invitation Accepters}}" + +msgid "controller.PetitionAcceptances" +msgstr "{0,plural,=1{Petition Acceptance} other{Petition Acceptances}}" + +msgid "controller.PetitionAttributes" +msgstr "{0,plural,=1{Petition Attribute} other{Petition Attributes}}" + +msgid "controller.PetitionBasicAttributeSets" +msgstr "{0,plural,=1{Petition Basic Attribute Set} other{Petition Basic Attribute Sets}}" + +msgid "controller.PetitionIdentifiers" +msgstr "{0,plural,=1{Petition Identifier} other{Petition Identifiers}}" + +msgid "controller.PetitionVerifications" +msgstr "{0,plural,=1{Petition Verification} other{Petition Verifications}}" + msgid "enumeration.VerificationModeEnum.0" msgstr "None" diff --git a/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php b/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php index 35595ccd6..23dd271df 100644 --- a/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php +++ b/app/plugins/CoreEnroller/src/Controller/EmailVerifiersController.php @@ -94,7 +94,8 @@ public function dispatch(string $id) { $verifiedAddresses = []; foreach($candidateAddresses as $a => $v) { - if(!empty($v->verification->verification_time)) { + // true indicates verified by the plugin that collected the address + if($v === true || !empty($v->verification->verification_time)) { $verifiedAddresses[$a] = true; } } diff --git a/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php b/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php index be0e63cf6..743bad07f 100644 --- a/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php +++ b/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php @@ -111,6 +111,7 @@ public function willHandleAuth(\Cake\Event\EventInterface $event): string { if($action == 'dispatch') { // We need to perform special logic (vs StandardEnrollerController) // to ensure that web server authentication is triggered. + // (This logic is also used in EnvSourceCollectorsController.) // To start, we trigger the parent logic. This will return // notauth: Some error occurred, we don't want to override this diff --git a/app/plugins/CoreEnroller/src/Model/Entity/AttributeCollector.php b/app/plugins/CoreEnroller/src/Model/Entity/AttributeCollector.php index 373b031a5..4a1432d03 100644 --- a/app/plugins/CoreEnroller/src/Model/Entity/AttributeCollector.php +++ b/app/plugins/CoreEnroller/src/Model/Entity/AttributeCollector.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class AttributeCollector extends Entity { + use \App\Lib\Traits\EntityMetaTrait; + /** * Fields that can be mass assigned using newEntity() or patchEntity(). * diff --git a/app/plugins/CoreEnroller/src/Model/Entity/BasicAttributeCollector.php b/app/plugins/CoreEnroller/src/Model/Entity/BasicAttributeCollector.php index 01f468029..675282e93 100644 --- a/app/plugins/CoreEnroller/src/Model/Entity/BasicAttributeCollector.php +++ b/app/plugins/CoreEnroller/src/Model/Entity/BasicAttributeCollector.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class BasicAttributeCollector extends Entity { + use \App\Lib\Traits\EntityMetaTrait; + /** * Fields that can be mass assigned using newEntity() or patchEntity(). * diff --git a/app/plugins/CoreEnroller/src/Model/Entity/EmailVerifier.php b/app/plugins/CoreEnroller/src/Model/Entity/EmailVerifier.php index f867a1c79..b22c3ebae 100644 --- a/app/plugins/CoreEnroller/src/Model/Entity/EmailVerifier.php +++ b/app/plugins/CoreEnroller/src/Model/Entity/EmailVerifier.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class EmailVerifier extends Entity { + use \App\Lib\Traits\EntityMetaTrait; + /** * Fields that can be mass assigned using newEntity() or patchEntity(). * diff --git a/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php b/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php index 85e00897e..024e76937 100644 --- a/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php +++ b/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class IdentifierCollector extends Entity { + use \App\Lib\Traits\EntityMetaTrait; + /** * Fields that can be mass assigned using newEntity() or patchEntity(). * diff --git a/app/plugins/CoreEnroller/src/Model/Entity/InvitationAccepter.php b/app/plugins/CoreEnroller/src/Model/Entity/InvitationAccepter.php index 8017d29b1..b5ac09d28 100644 --- a/app/plugins/CoreEnroller/src/Model/Entity/InvitationAccepter.php +++ b/app/plugins/CoreEnroller/src/Model/Entity/InvitationAccepter.php @@ -32,6 +32,8 @@ use Cake\ORM\Entity; class InvitationAccepter extends Entity { + use \App\Lib\Traits\EntityMetaTrait; + /** * Fields that can be mass assigned using newEntity() or patchEntity(). * diff --git a/app/plugins/CoreEnroller/src/Model/Table/BasicAttributeCollectorsTable.php b/app/plugins/CoreEnroller/src/Model/Table/BasicAttributeCollectorsTable.php index 780b8c066..a1aced150 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/BasicAttributeCollectorsTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/BasicAttributeCollectorsTable.php @@ -134,14 +134,14 @@ public function initialize(array $config): void { /** * Perform steps necessary to hydrate the Person record as part of Petition finalization. - * + * + * @since COmanage Registry v5.1.0 * @param int $id Basic Attribute Collector ID - * @param \App\Model\Entity\Petition $petition Petition + * @param Petition $petition Petition * @return bool true on success - * @since COmanage Registry v5.1.0 */ - public function finalize(int $id, \App\Model\Entity\Petition $petition) { + public function hydrate(int $id, \App\Model\Entity\Petition $petition) { $cfg = $this->get($id); // At this point there is a Person record allocated and stored in the Petition, @@ -323,12 +323,12 @@ public function validationDefault(Validator $validator): Validator { /** * Obtain the set of Email Addresses known to this plugin that are eligible for - * verification. + * verification or that have already been verified. * * @since COmanage Registry v5.1.0 * @param EntityInterface $config Configuration entity for this plugin * @param int $petitionId Petition ID - * @return array Array of Email Addrsses that are eligible for verification + * @return array Array of Email Addresses and verification status */ public function verifiableEmailAddresses( @@ -342,6 +342,6 @@ public function verifiableEmailAddresses( ]) ->first(); - return !empty($set->mail) ? [$set->mail] : []; + return !empty($set->mail) ? [$set->mail => false] : []; } } diff --git a/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php b/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php index 109940ca2..c515f1f0b 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/EmailVerifiersTable.php @@ -245,8 +245,12 @@ public function assembleVerifiableAddresses( $paddrs = $PluginTable->verifiableEmailAddresses($step->$pmodel, $petition->id); if(!empty($paddrs)) { - foreach($paddrs as $paddr) { - if(!array_key_exists($paddr, $ret)) { + foreach($paddrs as $paddr => $vstatus) { + if($vstatus) { + // The plugin asserts the address is verified, and is responsible for registering + // any Verifications + $ret[ $paddr ] = true; + } elseif(!array_key_exists($paddr, $ret)) { // Do we have a verification for this address? // This is basically copy/paste from above $verified = false; @@ -281,14 +285,14 @@ public function assembleVerifiableAddresses( /** * Perform steps necessary to hydrate the Person record as part of Petition finalization. - * + * + * @since COmanage Registry v5.1.0 * @param int $id Invitation Accepter ID - * @param \App\Model\Entity\Petition $petition Petition + * @param Petition $petition Petition * @return bool true on success - * @since COmanage Registry v5.1.0 */ - public function finalize(int $id, \App\Model\Entity\Petition $petition) { + public function hydrate(int $id, \App\Model\Entity\Petition $petition) { $cfg = $this->get($id); // At this point, the Steps that told us there are email addresses to verify diff --git a/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php b/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php index 45253a409..fbc2c3335 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php @@ -120,14 +120,14 @@ public function initialize(array $config): void { /** * Perform steps necessary to hydrate the Person record as part of Petition finalization. - * + * + * @since COmanage Registry v5.1.0 * @param int $id Invitation Accepter ID - * @param \App\Model\Entity\Petition $petition Petition + * @param Petition $petition Petition * @return bool true on success - * @since COmanage Registry v5.1.0 */ - public function finalize(int $id, \App\Model\Entity\Petition $petition) { + public function hydrate(int $id, \App\Model\Entity\Petition $petition) { $cfg = $this->get($id); // Pull the Identifier that was recorded diff --git a/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php b/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php index b696a9954..21587b228 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php @@ -113,14 +113,14 @@ public function initialize(array $config): void { /** * Perform steps necessary to hydrate the Person record as part of Petition finalization. - * + * + * @since COmanage Registry v5.1.0 * @param int $id Invitation Accepter ID - * @param \App\Model\Entity\Petition $petition Petition + * @param Petition $petition Petition * @return bool true on success - * @since COmanage Registry v5.1.0 */ - public function finalize(int $id, \App\Model\Entity\Petition $petition) { + public function hydrate(int $id, \App\Model\Entity\Petition $petition) { // $cfg = $this->get($id); // We don't have anything to do for finalization diff --git a/app/plugins/CoreEnroller/src/Model/Table/PetitionVerificationsTable.php b/app/plugins/CoreEnroller/src/Model/Table/PetitionVerificationsTable.php index 510dc7889..ecf39b690 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/PetitionVerificationsTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/PetitionVerificationsTable.php @@ -132,8 +132,6 @@ public function verifyCode(int $petitionId, int $enrollmentFlowStepId, string $m enrollmentFlowStepId: $enrollmentFlowStepId, action: PetitionActionEnum::EmailVerified, comment: __d('core_enroller', 'result.EmailVerifiers.verified.history', [$mail, __d('enumeration', 'VerificationMethodEnum.C')]) -// We don't have $actorPersonId yet... -// ?int $actorPersonId=null ); return true; @@ -170,8 +168,6 @@ public function verifyFromHandoff(int $petitionId, int $enrollmentFlowStepId, st enrollmentFlowStepId: $enrollmentFlowStepId, action: PetitionActionEnum::EmailVerified, comment: __d('core_enroller', 'result.EmailVerifiers.verified.history', [$mail, __d('enumeration', 'VerificationMethodEnum.PH')]) -// We don't have $actorPersonId yet... -// ?int $actorPersonId=null ); // We return in the format as if we used find() and contain() diff --git a/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php b/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php index 5e2e8aa41..60bc1f2d3 100644 --- a/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php +++ b/app/plugins/CoreJob/src/Lib/Jobs/SyncJob.php @@ -186,15 +186,20 @@ protected function fullSync() { if($this->runContext->eis->status == SyncModeEnum::Full) { $allKeys = $this->runContext->EISTable->inventory($this->runContext->eis->id); - $this->runContext->count = count($allKeys); + if($allKeys === false) { + $this->llog('error', "EIS " . $this->runContext->eis->description + . " configured for Full Sync but Plugin does not support inventory()"); + } else { + $this->runContext->count = count($allKeys); - $newKeys = array_diff($allKeys, $knownKeys); + $newKeys = array_diff($allKeys, $knownKeys); - foreach($newKeys as $sourceKey) { - $this->llog('trace', "EIS " . $this->runContext->eis->description - . " processing new entry $sourceKey"); - - $this->syncRecord((string)$sourceKey); + foreach($newKeys as $sourceKey) { + $this->llog('trace', "EIS " . $this->runContext->eis->description + . " processing new entry $sourceKey"); + + $this->syncRecord((string)$sourceKey); + } } } diff --git a/app/resources/locales/en_US/command.po b/app/resources/locales/en_US/command.po index 332654692..c4e0296fb 100644 --- a/app/resources/locales/en_US/command.po +++ b/app/resources/locales/en_US/command.po @@ -135,6 +135,21 @@ msgstr "Message Template ID" msgid "opt.notify.type" msgstr "Type (label) for provided Identifier" +msgid "opt.upgrade.forcecurrent" +msgstr "Force the specified current version -- ADVANCED USERS ONLY" + +msgid "opt.upgrade.task" +msgstr "Upgrade task to perform -- ADVANCED USERS ONLY" + +msgid "opt.upgrade.skipdatabase" +msgstr "Skip database schema update -- ADVANCED USERS ONLY" + +msgid "opt.upgrade.skipvalidation" +msgstr "Skip version validation -- ADVANCED USERS ONLY" + +msgid "opt.upgrade.version" +msgstr "Version to upgrade to (default: current release)" + # msgid "se.admin" # msgstr "Creating initial administrator permission" diff --git a/app/resources/locales/en_US/controller.po b/app/resources/locales/en_US/controller.po index b4fab7b4a..f63a9ef83 100644 --- a/app/resources/locales/en_US/controller.po +++ b/app/resources/locales/en_US/controller.po @@ -144,6 +144,9 @@ msgstr "{0,plural,=1{Server} other{Servers}}" msgid "TelephoneNumbers" msgstr "{0,plural,=1{Telephone Number} other{Telephone Numbers}}" +msgid "TrafficDetours" +msgstr "{0,plural,=1{Traffic Detour} other{Traffic Detours}}" + msgid "Types" msgstr "{0,plural,=1{Type} other{Types}}" diff --git a/app/resources/locales/en_US/enumeration.po b/app/resources/locales/en_US/enumeration.po index 691a14338..f58d0af2b 100644 --- a/app/resources/locales/en_US/enumeration.po +++ b/app/resources/locales/en_US/enumeration.po @@ -456,6 +456,9 @@ msgstr "Email Verified" msgid "PetitionActionEnum.F" msgstr "Finalized" +msgid "PetitionActionEnum.FD" +msgstr "Flagged Duplicate" + msgid "PetitionActionEnum.IV" msgstr "Invitation Viewed" diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index ad8478207..dc8c648e3 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -235,6 +235,9 @@ msgstr "Provided value is not an integer" msgid "Jobs.plugin.parameter.invalid" msgstr "Invalid parameter" +msgid "Jobs.plugin.parameter.required" +msgstr "Required parameter not provided" + msgid "Jobs.plugin.parameter.select" msgstr "Provided value is not a valid choice" @@ -373,6 +376,21 @@ msgstr "Type {0} is in use as a default (via CO Settings)" msgid "unknown" msgstr "Unknown value \"{0}\"" +msgid "ug.task.unknown" +msgstr "Task {0} is not defined" + +msgid "ug.version.blocked" +msgstr "Cannot automatically upgrade past version {0}, please upgrade to that version first" + +msgid "ug.version.order" +msgstr "Target version is before current version (cannot downgrade)" + +msgid "ug.version.same" +msgstr "Current and target versions are the same" + +msgid "ug.version.unknown" +msgstr "Unknown version \"{0}\"" + msgid "Verifications.already" msgstr "Email Address is already verified" diff --git a/app/resources/locales/en_US/field.po b/app/resources/locales/en_US/field.po index 35592d08f..f24603317 100644 --- a/app/resources/locales/en_US/field.po +++ b/app/resources/locales/en_US/field.po @@ -59,6 +59,9 @@ msgstr "Comment" msgid "context" msgstr "Context" +msgid "copy-a" +msgstr "Copy of {0}" + msgid "country" msgstr "Country" @@ -441,6 +444,9 @@ msgstr "Petitioner Authorization" msgid "EnrollmentFlows.collect_enrollee_email" msgstr "Collect Enrollee Email" +msgid "EnrollmentFlows.redirect_on_duplicate" +msgstr "Redirect on Duplicate" + msgid "EnrollmentFlows.redirect_on_finalize" msgstr "Redirect on Finalize" @@ -638,6 +644,15 @@ msgstr "The full (public) URL to access the rendered page (this is a read only f # These are strings for the default Pages that are created for each CO. # It's not clear they belong here, but we don't have a better place for them right now. +msgid "MostlyStaticPages.default.de.title" +msgstr "Duplicate Enrollment" + +msgid "MostlyStaticPages.default.de.description" +msgstr "Default Duplicate Enrollment Landing Page" + +msgid "MostlyStaticPages.default.de.body" +msgstr "This Petition appears to be a duplicate, and this Enrollment Flow has been stopped. Please contact your administrator for further assistance." + msgid "MostlyStaticPages.default.dh.title" msgstr "Enrollment Flow Handoff" @@ -695,6 +710,12 @@ msgstr "Resolution Time" msgid "Notifications.subject_person_id" msgstr "Subject" +msgid "Petitions.enrollee_email" +msgstr "Enrollee Email Address" + +msgid "Petitions.enrollee_email.desc" +msgstr "The Email Address provided at the creation of the Petition" + msgid "Petitions.enrollee.new" msgstr "New Enrollee" @@ -746,6 +767,12 @@ msgstr "Role Status On Delete" msgid "Pipelines.sync_status_on_delete.desc" msgstr "When the source record is no longer valid, the corresponding Person Role will be set to this status" +msgid "Pipelines.sync_verify_email_addresses" +msgstr "Flag All Email Addresses Verified" + +msgid "Pipelines.sync_verify_email_addresses.desc" +msgstr "If true, all email addresses processed by this Pipeline will be considered verified" + msgid "Plugins.plugin" msgstr "Plugin" diff --git a/app/resources/locales/en_US/information.po b/app/resources/locales/en_US/information.po index 6c890c386..d9460126c 100644 --- a/app/resources/locales/en_US/information.po +++ b/app/resources/locales/en_US/information.po @@ -156,5 +156,20 @@ msgstr "Report for " msgid "table.list" msgstr "{0} List" +msgid "ug.tasks.post" +msgstr "Executing post-database tasks for version {0}" + +msgid "ug.tasks.pre" +msgstr "Executing pre-database tasks for version {0}" + +msgid "ug.version.current" +msgstr "Current version: {0}" + +msgid "ug.version.target" +msgstr "Target version: {0}" + +msgid "ug.installMostlyStaticPages" +msgstr "Installing default Mostly Static Pages for CO {0}" + msgid "value.copied" msgstr "Value copied to clipboard." \ No newline at end of file diff --git a/app/resources/locales/en_US/result.po b/app/resources/locales/en_US/result.po index dfd031ceb..c02348d32 100644 --- a/app/resources/locales/en_US/result.po +++ b/app/resources/locales/en_US/result.po @@ -36,6 +36,9 @@ msgstr "Ad hoc attribute deleted" msgid "applied.schema" msgstr "Successfully applied database schema" +msgid "copied" +msgstr "Copied" + msgid "deactivated" msgstr "{0} Deactivated" @@ -54,8 +57,20 @@ msgstr "{0} {1} Edited: {2}" msgid "EmailAddress.deleted" msgstr "Email address deleted" -msgid "EmailAddresses.verify.forced" -msgstr "Email Address force verified" +msgid "EmailAddresses.verify.code" +msgstr "Email Address {0} verified by code" + +msgid "EmailAddresses.verify.code.sent" +msgstr "Verification code sent to Email Address {0}" + +msgid "EmailAddresses.verify.handoff" +msgstr "Email Address {0} verified by handoff" + +msgid "EmailAddresses.verify.manual" +msgstr "Email Address {0} force verified" + +msgid "EmailAddresses.verify.trust" +msgstr "Email Address {0} verified by trusted source {1}" msgid "ExternalIdentities.status.recalculated" msgstr "External Identity status recalculated from {0} to {1}" @@ -166,17 +181,23 @@ msgstr "Person Role status recalculated from {0} to {1}" msgid "Petitions.finalized" msgstr "Petition Finalized" +msgid "Petitions.flaggedduplicate" +msgstr "Petition flagged as Duplicate" + msgid "Petitions.viewed.inv" msgstr "Invitation Viewed" msgid "Pipelines.complete" msgstr "Pipeline {0} complete for EIS {1} source key {2}" +msgid "Pipelines.ei.added" +msgstr "Created new External Identity via Pipeline {0} ({1}) using Source {2} ({3}) Key {4}" + msgid "Pipelines.matched" msgstr "Pipeline {0} ({1}) matched EIS {2} ({3}) source key {4} to Person using Match Strategy {5}" -msgid "Pipelines.ei.added" -msgstr "Created new External Identity via Pipeline {0} ({1}) using Source {2} ({3}) Key {4}" +msgid "Pipelines.requested" +msgstr "Pipeline {0} ({1}) linked to requested Person {2} for EIS {3} ({4}) source key {5}" msgid "Pipelines.started" msgstr "Pipeline {0} ({1}) started for EIS {2} ({3}) source key {4}" @@ -230,6 +251,9 @@ msgstr "Database connection established" msgid "test.mail.ok" msgstr "Test message sent" +msgid "ug.task.done" +msgstr "Finished processing task {0}" + msgid "updated" msgstr "updated" diff --git a/app/src/Command/SetupCommand.php b/app/src/Command/SetupCommand.php index e8218d55c..a273399d6 100644 --- a/app/src/Command/SetupCommand.php +++ b/app/src/Command/SetupCommand.php @@ -205,6 +205,10 @@ public function execute(Arguments $args, ConsoleIo $io) } } + // Set the current version in the meta table + $metaTable = $this->getTableLocator()->get('Meta'); + $metaTable->setUpgradeVersion(); + $io->out(__d('command', 'se.done')); } } \ No newline at end of file diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index a332ce577..37871a6bb 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -554,10 +554,9 @@ public function execute(Arguments $args, ConsoleIo $io) { $atables = $args->getArguments(); // Register the current version for future upgrade purposes - $targetVersion = rtrim(file_get_contents(CONFIG . DS . "VERSION")); $metaTable = $this->getTableLocator()->get('Meta'); - $metaTable->setUpgradeVersion($targetVersion, true); + $metaTable->setUpgradeVersion(); foreach(array_keys($this->tables) as $t) { // If the command line args include a list of tables skip this table diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 4ac062f82..07c27cd42 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -30,7 +30,7 @@ namespace App\Controller; use App\Lib\Enum\TemplateableStatusEnum; -use App\Lib\Events\ChangelogEventListener; +use App\Lib\Events\ActorEventListener; use App\Lib\Events\CoIdEventListener; use App\Lib\Events\RuleBuilderEventListener; use App\Lib\Util\StringUtilities; @@ -83,8 +83,8 @@ public function initialize(): void { // Breadcrumb Manager $this->loadComponent('Breadcrumb'); - $ChangelogEventListener = new ChangelogEventListener($this->RegistryAuth); - EventManager::instance()->on($ChangelogEventListener); + $ActorEventListener = new ActorEventListener($this->RegistryAuth); + EventManager::instance()->on($ActorEventListener); $RuleBuilderEventListener = new RuleBuilderEventListener(); EventManager::instance()->on($RuleBuilderEventListener); @@ -165,6 +165,7 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { */ public function beforeRender(\Cake\Event\EventInterface $event) { + // $this->name = Models $modelsName = $this->name; // Views can also inspect the request object to determine the current @@ -678,10 +679,8 @@ protected function setCO() { // $modelsName may not be set, so (eg) StandardApiController does // something similar. - // This only works for the current model, not related models. If/when we - // need to support relatedmodels, we could have setCurCoId() cascade the - // CO to any of its related models that require it, or use the event - // listener approach commented out below. + // This only works for the current model, not related models. For + // relatedmodels, we use the event listener approach below. if(method_exists($this->$modelsName, "acceptsCoId") && $this->$modelsName->acceptsCoId()) { $this->$modelsName->setCurCoId((int)$coid); diff --git a/app/src/Controller/Component/RegistryAuthComponent.php b/app/src/Controller/Component/RegistryAuthComponent.php index 4c74ddf6d..2a0cd0cc9 100644 --- a/app/src/Controller/Component/RegistryAuthComponent.php +++ b/app/src/Controller/Component/RegistryAuthComponent.php @@ -280,7 +280,7 @@ public function beforeFilter(EventInterface $event) { // We want to come back to where we started $session->write('Auth.target', $request->getRequestTarget()); - return $controller->redirect("/auth/login/login.php"); + return $controller->redirect("/traffic/prepare-login"); } } diff --git a/app/src/Controller/DashboardsController.php b/app/src/Controller/DashboardsController.php index f027da29a..9a133edde 100644 --- a/app/src/Controller/DashboardsController.php +++ b/app/src/Controller/DashboardsController.php @@ -163,7 +163,9 @@ public function configuration() { $platformMenuItems = []; - if($this->getCOID() == 1) { + $co = $this->getCO(); + + if($co->isCOmanageCO()) { // Also pass the platform menu items $platformMenuItems = [ @@ -176,6 +178,11 @@ public function configuration() { 'icon' => 'electrical_services', 'controller' => 'plugins', 'action' => 'index' + ], + __d('controller', "TrafficDetours", [99]) => [ + 'icon' => 'fork_right', + 'controller' => 'traffic_detours', + 'action' => 'index' ] ]; } diff --git a/app/src/Controller/EmailAddressesController.php b/app/src/Controller/EmailAddressesController.php index 0ed2cfe88..ab962178c 100644 --- a/app/src/Controller/EmailAddressesController.php +++ b/app/src/Controller/EmailAddressesController.php @@ -48,8 +48,8 @@ class EmailAddressesController extends MVEAController { public function forceVerify(string $id) { try { - $this->EmailAddresses->forceVerify((int)$id, $this->RegistryAuth->getPersonID($this->getCOID())); - $this->Flash->success(__d('result', 'EmailAddresses.verify.forced')); + $addr = $this->EmailAddresses->forceVerify((int)$id, $this->RegistryAuth->getPersonID($this->getCOID())); + $this->Flash->success(__d('result', 'EmailAddresses.verify.manual', [$addr])); } catch(Exception $e) { $this->Flash->error($e->getMessage()); diff --git a/app/src/Controller/EnrollmentFlowsController.php b/app/src/Controller/EnrollmentFlowsController.php index 79f043fbe..9a2afb8fa 100644 --- a/app/src/Controller/EnrollmentFlowsController.php +++ b/app/src/Controller/EnrollmentFlowsController.php @@ -21,7 +21,7 @@ * * @link https://www.internet2.edu/comanage COmanage Project * @package registry - * @since COmanage Registry v5.0.0 + * @since COmanage Registry v5.1.0 * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ @@ -45,7 +45,7 @@ class EnrollmentFlowsController extends StandardController { /** * Calculate authorization for the current request. * - * @since COmanage Registry v5.0.0 + * @since COmanage Registry v5.1.0 * @return bool True if the current request is permitted, false otherwise */ @@ -101,9 +101,35 @@ public function calculatePermission(): bool { } /** - * Start an Enrollment flow. + * Copy an Enrollment Flow. * - * @since COmanage Registry v5.0.0 + * @since COmanage Registry v5.1.0 + * @param string $id Enrollment Flow ID + */ + + public function copy(string $id) { + try { + $related = [ + 'EnrollmentFlowSteps' => $this->EnrollmentFlows->EnrollmentFlowSteps->getPluginRelations() + ]; + + $obj = $this->EnrollmentFlows->copy((int)$id, $related); + $this->Flash->success(__d('result', 'copied')); + + // Redirect to the newly created flow + return $this->generateRedirect($obj); + } + catch(\Exception $e) { + $this->Flash->error($e->getMessage()); + } + + return $this->generateRedirect(null); + } + + /** + * Start an Enrollment Flow. + * + * @since COmanage Registry v5.1.0 * @param string $id Enrollment Flow ID */ @@ -168,7 +194,7 @@ public function start(string $id) { /** * Indicate whether this Controller will handle some or all authnz. * - * @since COmanage Registry v5.0.0 + * @since COmanage Registry v5.1.0 * @param EventInterface $event Cake event, ie: from beforeFilter * @return string "no", "open", "authz", or "yes" */ diff --git a/app/src/Controller/PetitionsController.php b/app/src/Controller/PetitionsController.php index d6360cf8d..223d5863c 100644 --- a/app/src/Controller/PetitionsController.php +++ b/app/src/Controller/PetitionsController.php @@ -127,8 +127,9 @@ public function finalize(string $id) { // as expected. We use an 'op' flag rather than separate actions in order to simplify // the authorization logic (which is already custom for finalize). - // finalize: Tell all plugins to finalize + // hydrate: Tell all plugins to hydrate (construct) the Person record // assign: Assign Identifiers (if any) + // derive: Tell all plugins to perform any derivations from the Person record // provision: Run provisioning, then set petition status to Finalized $op = $this->requestParam('op'); @@ -146,26 +147,13 @@ public function finalize(string $id) { } if(!$op) { - $op = 'finalize'; + $op = 'hydrate'; } - $resumeUrl = [ - 'plugin' => null, - 'controller' => 'petitions', - 'action' => 'resume', - (int)$id - ]; - try { - if($op == 'finalize') { + if($op == 'hydrate') { // Step 1 - try { - $this->Petitions->finalizePlugins((int)$id); - } catch (\Exception $e) { - $this->Flash->error($e->getMessage()); - // Get me back to the resume page - return $this->redirect($resumeUrl); - } + $this->Petitions->hydrate((int)$id); // Next operation is assign $baseUrl['?']['op'] = 'assign'; @@ -175,12 +163,20 @@ public function finalize(string $id) { // Step 2 $this->Petitions->assignIdentifiers((int)$id); + // Next operation is derive + $baseUrl['?']['op'] = 'derive'; + + return $this->redirect($baseUrl); + } elseif($op == 'derive') { + // Step 3 + $this->Petitions->derive((int)$id); + // Next operation is provision $baseUrl['?']['op'] = 'provision'; return $this->redirect($baseUrl); } elseif($op == 'provision') { - // Step 3 + // Step 4 $this->Petitions->provision((int)$id); // We're really done now, update the Petition status and redirect appropriately @@ -203,6 +199,24 @@ public function finalize(string $id) { throw new \InvalidArgumentException(__d('error', 'unknown', $op)); } } + catch(\OverflowException $e) { + // Note we use the general Flow redirect on duplicate here, not any plugin specific + // configuration (eg: EnvSource's duplicate redirect URL) since in theory any plugin + // can trigger this. + + $petition = $this->Petitions->get((int)$id, ['contain' => ['EnrollmentFlows']]); + + // Redirect to configured URL or default location + if(!empty($petition->enrollment_flow->redirect_on_duplicate)) { + // Use the Enrollment Flow specific redirect URL + return $this->redirect($petition->enrollment_flow->redirect_on_duplicate); + } else { + // Redirect to the default Duplicate Landing URL for this CO + $coId = $this->getCOID(); + + return $this->redirect("/$coId/duplicate-landing"); + } + } catch(\Exception $e) { $this->Flash->error($e->getMessage()); } diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 798b3cd80..4d5844905 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -192,17 +192,16 @@ public function beforeRender(\Cake\Event\EventInterface $event) { // Populate permissions info, which uses the requested object ID if one // was provided. As a first approximation, those actions that permit lookup // primary link are also those that pass an $id that can be used to establish - // permissions, and also Cos (which has no primary link). + // permissions. We also allow any action for models without primary links + // (eg Cos, Plugins, and TrafficDetours). $id = null; $params = $this->request->getParam('pass'); if(!empty($params[0])) { - if((method_exists($table, "allowLookupPrimaryLink") - && $table->allowLookupPrimaryLink($this->request->getParam('action'))) - || - $modelsName == 'Cos') { + if(!method_exists($table, "allowLookupPrimaryLink") + || $table->allowLookupPrimaryLink($this->request->getParam('action'))) { $id = (int)$params[0]; } } @@ -507,7 +506,7 @@ public function generateRedirect($entity) { return $this->redirect(['action' => 'deleted']); } elseif($redirectGoal == 'self' && $entity - && in_array($this->request->getParam('action'), ['add', 'edit'])) { + && in_array($this->request->getParam('action'), ['add', 'copy', 'edit'])) { // We typically want to redirect to the edit view of the record, // but in some cases (eg: if the record was just frozen) we want to // redirect to "view" instead. diff --git a/app/src/Controller/StandardEnrollerController.php b/app/src/Controller/StandardEnrollerController.php index 918a3a70a..ebe7b4446 100644 --- a/app/src/Controller/StandardEnrollerController.php +++ b/app/src/Controller/StandardEnrollerController.php @@ -193,7 +193,7 @@ public function getPetition(): ?\App\Model\Entity\Petition { * * @since COmanage Registry v5.1.0 * @param EventInterface $event Cake event, ie: from beforeFilter - * @return string "no", "open", "authz", or "yes" + * @return string "no", "open", "authz", "yes", or "notauth" */ public function willHandleAuth(\Cake\Event\EventInterface $event): string { @@ -217,7 +217,7 @@ public function willHandleAuth(\Cake\Event\EventInterface $event): string { if(empty($modelId)) { $this->llog('error', "No step ID specified for dispatch"); - return 'noauth'; + return 'notauth'; } $stepConfig = $this->$modelsName->get($modelId, ['contain' => 'EnrollmentFlowSteps']); diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php index 53f96cc35..27def58d4 100644 --- a/app/src/Controller/StandardPluginController.php +++ b/app/src/Controller/StandardPluginController.php @@ -60,13 +60,15 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { $this->Breadcrumb->skipParents(['/^\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+\/edit\//']); - if($primaryLink->attr == 'server_id') { - // Servers shouldn't show up as configuration, so automatically hide it - // eg for server plugins - $this->Breadcrumb->skipConfig(['/^\//']); + if(!empty($primaryLink->attr)) { + if($primaryLink->attr == 'server_id') { + // Servers shouldn't show up as configuration, so automatically hide it + // eg for server plugins + $this->Breadcrumb->skipConfig(['/^\//']); + } + + $this->Breadcrumb->injectPrimaryLink($primaryLink); } - - $this->Breadcrumb->injectPrimaryLink($primaryLink); } return parent::beforeFilter($event); diff --git a/app/src/Controller/TrafficController.php b/app/src/Controller/TrafficController.php index 88024c1f5..e3b8e1b89 100644 --- a/app/src/Controller/TrafficController.php +++ b/app/src/Controller/TrafficController.php @@ -32,41 +32,121 @@ use Cake\Controller\Controller; use Cake\ORM\TableRegistry; use \App\Lib\Enum\AuthenticationEventEnum; +use \App\Lib\Enum\SuspendableStatusEnum; +use \App\Lib\Util\StringUtilities; // XXX not doing anything with Log yet use \Cake\Log\Log; class TrafficController extends Controller { + /** + * Initiate a login transaction. + * + * @since COmanage Registry v5.1.0 + */ + + public function initiateLogin() { +// Update https://spaces.at.internet2.edu/display/COmanage/Registry+PE+Installation+-+Source#RegistryPEInstallationSource-IntegrateWebServerAuthentication +// when plugin support is added here + +/* +We only need to pass through here once. Either we find a detour that handles login context +and redirect to it, or we don't and we redirect to the standard login handler. Any plugin +that handles login should + + (1) Set $_SESSION['Auth']['external']['user'] to the authenticated identifier + (2) Redirect to /registry/traffic/process-login + +(This is the same work done in webroot/auth/login/login.php, perhaps we should provide +a utility to facilitate) + + $TrafficDetours = TableRegistry::getTableLocator()->get('TrafficDetours'); + + $detour = $TrafficDetours->calculateNextDetour(context: 'login'); + + if(!empty($detour)) { + // Redirect into the detour. We only support one plugin handling the login context, + // and expect that plugin will redirect to process-login. + } +*/ + + // If we get here, use the default webserver login handler + return $this->redirect("/auth/login/login.php"); + } + + /** + * Prepare for a login action. + * + * @since COmanage Registry v5.1.0 + */ + + public function prepareLogin() { + // XXX Add support for calling plugins in "prelogin" context + + return $this->redirect("/traffic/initiate-login"); + } + /** * Process a login action on return from auth/login/login.php. * * @since COmanage Registry v5.0.0 */ - + public function processLogin() { $request = $this->getRequest(); $session = $request->getSession(); + + $lastDetourId = (int)$this->request->getQuery("done"); + + // The next detour to run, once determined + $detour = null; + + $TrafficDetours = TableRegistry::getTableLocator()->get('TrafficDetours'); - $username = $session->read('Auth.external.user'); - - if(!$username) { - throw new \InvalidArgumentException('Auth.external.user not found in TrafficController'); + if(!empty($lastDetourId)) { + // Figure out the next Traffic Detour to run + + // What's the next detour? + $detour = $TrafficDetours->calculateNextDetour(context: 'postlogin', lastDetourId: $lastDetourId); + } else { + // This is our first time through, process the login and then redirect appropriately + + $username = $session->read('Auth.external.user'); + + if(!$username) { + throw new \InvalidArgumentException('Auth.external.user not found in TrafficController'); + } + + // Record the login event + $AuthenticationEvents = TableRegistry::getTableLocator()->get('AuthenticationEvents'); + + $AuthenticationEvents->record(identifier: $username, + eventType: AuthenticationEventEnum::RegistryLogin, + remoteIp: $_SERVER['REMOTE_ADDR']); + + // What's the next detour? + $detour = $TrafficDetours->calculateNextDetour(context: 'postlogin'); } - + + if($detour) { + // Redirect into this detour + + return $this->redirect([ + 'plugin' => StringUtilities::PluginPlugin($detour->plugin), + 'controller' => StringUtilities::PluginModel($detour->plugin), + 'action' => 'postlogin', + '?' => ['detour_id' => $detour->id] + ]); + } + + // We're done with postlogin handling, redirect to the original target the user + // was trying to get to $target = $session->read('Auth.target'); if(!$target) { throw new \InvalidArgumentException('Auth.target not found in TrafficController'); } - // Record the login event - $AuthenticationEvents = TableRegistry::getTableLocator()->get('AuthenticationEvents'); - - $AuthenticationEvents->record(identifier: $username, - eventType: AuthenticationEventEnum::RegistryLogin, - remoteIp: $_SERVER['REMOTE_ADDR']); - - // Redirect to $target return $this->redirect($target); } } \ No newline at end of file diff --git a/app/src/Lib/Enum/ActionEnum.php b/app/src/Lib/Enum/ActionEnum.php index c7752ef70..65c38e173 100644 --- a/app/src/Lib/Enum/ActionEnum.php +++ b/app/src/Lib/Enum/ActionEnum.php @@ -33,7 +33,10 @@ class ActionEnum extends StandardEnum { // Codes beginning with 'X' (eg: 'XABC') are reserved for local use // Codes beginning with a lowercase 'p' (eg: 'pABC') are reserved for plugin use const CommentAdded = 'CMNT'; - const EmailForceVerified = 'EAFV'; + const EmailForceVerified = 'EMFV'; + const EmailVerified = 'EMLV'; + const EmailVerifyCodeSent = 'EMLS'; + const ExternalIdentityLoginUpdate = 'EOIE'; const GroupAdded = 'ACGR'; const GroupDeleted = 'DCGR'; const GroupEdited = 'ECGR'; diff --git a/app/src/Lib/Enum/PetitionActionEnum.php b/app/src/Lib/Enum/PetitionActionEnum.php index 88f713934..54715498c 100644 --- a/app/src/Lib/Enum/PetitionActionEnum.php +++ b/app/src/Lib/Enum/PetitionActionEnum.php @@ -33,6 +33,7 @@ class PetitionActionEnum extends StandardEnum { const AttributesUpdated = 'AU'; const EmailVerified = 'EV'; const Finalized = 'F'; + const FlaggedDuplicate = 'FD'; const InvitationViewed = 'IV'; const StatusUpdated = 'SU'; } \ No newline at end of file diff --git a/app/src/Lib/Events/ChangelogEventListener.php b/app/src/Lib/Events/ChangelogEventListener.php deleted file mode 100644 index 833476443..000000000 --- a/app/src/Lib/Events/ChangelogEventListener.php +++ /dev/null @@ -1,87 +0,0 @@ -RegistryAuth = $Auth; - } - - /** - * Before save event listener. - * - * @since COmanage Registry v5.0.0 - * @param Event $event Cake Event - * @param EntityInterface $entity Entity subject of the event (ie: object to be saved) - * @param ArrayObject $options Save options - */ - - public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options) { - // Tweak the options so ChangelogBehavior can see who performed the save - - if($this->RegistryAuth) { - $options['actor'] = $this->RegistryAuth->getAuthenticatedUser(); - $options['apiuser'] = $this->RegistryAuth->isApiUser(); - } - } - - /** - * Define the list of implemented events. - * - * @since COmanage Registry v5.0.0 - * @return array Array of implemented events and associated configuration. - */ - - public function implementedEvents(): array { - return [ - 'Model.beforeSave' => [ - 'callable' => 'beforeSave', - // We need this beforeSave to run before the ChangelogBehavior beforeSave - 'priority' => -100 - ] - ]; - } -} diff --git a/app/src/Lib/Events/CoIdEventListener.php b/app/src/Lib/Events/CoIdEventListener.php index aadb65c0f..a6ad2305c 100644 --- a/app/src/Lib/Events/CoIdEventListener.php +++ b/app/src/Lib/Events/CoIdEventListener.php @@ -42,10 +42,10 @@ class CoIdEventListener Implements EventListenerInterface { * Constructor. * * @since COmanage Registry v5.0.0 - * @param XXX + * @param int $coId CO ID */ - public function __construct(int $coId) { + public function __construct(?int $coId=null) { $this->coId = $coId; } @@ -81,4 +81,16 @@ public function implementedEvents(): array { ] ]; } + + /** + * Update the CO ID. This call is intended for contests where records from multiple COs + * are being processed within a single task. + * + * @since COmange Registry v5.1.0 + * @param int $coId CO ID + */ + + public function updateCoId(int $coId) { + $this->coId = $coId; + } } diff --git a/app/src/Lib/Traits/EnrollmentControllerTrait.php b/app/src/Lib/Traits/EnrollmentControllerTrait.php index d4bfb5f59..c9830a390 100644 --- a/app/src/Lib/Traits/EnrollmentControllerTrait.php +++ b/app/src/Lib/Traits/EnrollmentControllerTrait.php @@ -62,6 +62,18 @@ protected function getCurrentActor(?int $petitionId=null): array { 'petition' => null ]; + if(empty($ret['identifier'])) { + // Under certain circumstances (eg: EnvSource::dispatch) we may run before + // RegistryAuth::beforeFilter, in which case getAuthenticatedUser() won't have + // an authenticated user yet. As a workaround, we manually read the session to see + // if an authenticated identifier has been set. + + $request = $this->getRequest(); + $session = $request->getSession(); + + $ret['identifier'] = $session->read('Auth.external.user'); + } + if(!empty($ret['identifier'])) { // Can we map this identifier to a Person ID? @@ -106,12 +118,13 @@ protected function getCurrentActor(?int $petitionId=null): array { $ret['roles'][] = EnrollmentActorEnum::Enrollee; } - if(empty($petition->petitioner_identifier) - && empty($petition->enrollee_identifier)) { + if(//empty($petition->petitioner_identifier) && + //in_array(EnrollmentActorEnum::Enrollee, $ret['roles']) + empty($petition->enrollee_identifier)) { // We have an identifier at run time but none in the petition. // If we can validate a token we can store the identifier and // use it instead. (eg: An Enrollee receives an initial handoff - // email/invitation.) + // email/invitation, or an Enrollee is asked to authenticate.) // Note in general we should only accept an Enrollee identifier // this way. Petitioner identifiers should be collected at Petition diff --git a/app/src/Lib/Traits/EntityMetaTrait.php b/app/src/Lib/Traits/EntityMetaTrait.php index bd8d7ddf3..6545dd924 100644 --- a/app/src/Lib/Traits/EntityMetaTrait.php +++ b/app/src/Lib/Traits/EntityMetaTrait.php @@ -32,6 +32,21 @@ use Cake\Utility\Inflector; trait EntityMetaTrait { + /** + * Determine the changelog attribute foreign key (eg: name_id for Name) for this entity. + * + * @since COmanage Registry v5.1.0 + * @return string Changelog attribute column name + */ + + public function changelogAttributeName() { + // The class name is something like `\App\Model\Entity\TelephoneNumber', but we + // want telephone_number (lowercased). + $entityName = Inflector::underscore(substr(strrchr(get_class($this), '\\'),1)); + + return $entityName . "_id"; + } + /** * Determine if the record described in $data is probably the same as the * current value of the Entity. This is intended to support Pipelines in diff --git a/app/src/Lib/Traits/PluggableModelTrait.php b/app/src/Lib/Traits/PluggableModelTrait.php index 10a5c1a52..cbf0458d9 100644 --- a/app/src/Lib/Traits/PluggableModelTrait.php +++ b/app/src/Lib/Traits/PluggableModelTrait.php @@ -30,6 +30,7 @@ namespace App\Lib\Traits; use Cake\ORM\ResultSet; +use Cake\ORM\TableRegistry; use Cake\Utility\Inflector; use App\Lib\Util\StringUtilities; @@ -38,6 +39,47 @@ trait PluggableModelTrait { // The set of plugin entry point models used in configurations for this model protected $_pluginModels = []; + /** + * Callback after data is marshaled into an entity. + * + * @since COmanage Registry v5.1.0 + * @param EventInterface $event afterMarshal event + * @param Entity Interface $entity Marshalled entity + * @param ArrayObject $data Entity data + * @param ArrayObject $options Callback options + */ + + public function afterMarshal( + \Cake\Event\EventInterface $event, + $entity, //\Cake\Event\EntityInterface $entity, + \ArrayObject $data, + \ArrayObject $options + ) { + // For some reason Cake doesn't seem to marshal our related plugin model, + // possibly because it can't find the table to create the new entity. + // If we see we have a plugin defined and an array of data, convert it to + // an entity instead. (CopyTrait relies on this behavior.) + + if(!empty($entity->plugin)) { + // The plugin is the full model path, eg CoreEnroller.InvitationAccepters. + // Since plugins all have a hasOne relation, we need to convert that to + // the singular form (eg invitation_accepter). + + // Get the plugin component of the path and lowercase it + $m = Inflector::singularize(Inflector::underscore(StringUtilities::pluginModel($entity->plugin))); + + if(!empty($entity->$m) && is_array($entity->$m)) { + // Convert this array to an entity. We'll need to obtain the table, too + + $PluginTable = TableRegistry::getTableLocator()->get($entity->plugin); + + $entity->$m = $PluginTable->newEntity($entity->$m); + +// XXX CFM-127, CFM-31 when plugins want to do more complex operations on duplicate, add it here + } + } + } + /** * Determine the plugin type used by this Pluggable Model. This is the lowercased * singular prefix of the Pluggable Model Table name. eg: For "ReportsTable" the @@ -168,9 +210,11 @@ protected function setPluginRelations() { // isArtifactTable() might not be the exact right test here... // for now, we only want to exclude Jobs (since there's nothing - // to configure) but this may change. + // to configure) but this may change. Also, Traffic Detours don't + // have a primary link. - if(!$this->isArtifactTable()) { + if(!$this->isArtifactTable() + && method_exists($this, 'setAllowLookupPrimaryLink')) { $this->setAllowLookupPrimaryLink(['configure']); } } diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php index 32bce696c..ddf347925 100644 --- a/app/src/Lib/Traits/PrimaryLinkTrait.php +++ b/app/src/Lib/Traits/PrimaryLinkTrait.php @@ -138,7 +138,10 @@ public function calculateCoForRecord(EntityInterface $entity, bool $original=fal $linkValue = ($original ? $entity->getOriginal($lf) : $entity->get($lf)); - return $LinkTable->findCoForRecord($linkValue); + if(method_exists($LinkTable, 'findCoForRecord')) { + return $LinkTable->findCoForRecord($linkValue); + } + // else Platform plugins (eg: Traffic Detours) don't belong to a CO } } } diff --git a/app/src/Lib/Traits/TableMetaTrait.php b/app/src/Lib/Traits/TableMetaTrait.php index 39d4fd88a..671503ea1 100644 --- a/app/src/Lib/Traits/TableMetaTrait.php +++ b/app/src/Lib/Traits/TableMetaTrait.php @@ -29,6 +29,7 @@ namespace App\Lib\Traits; +use Cake\ORM\TableRegistry; use Cake\Utility\Inflector; use App\Lib\Enum\TableTypeEnum; use App\Lib\Util\StringUtilities; @@ -37,6 +38,130 @@ trait TableMetaTrait { // What type of Table is this? private $tableType = null; + /** + * Filter the metadata attributes from an entity in a manner suitable for copy (duplicate). + * + * @since COmanage Registry v5.1.0 + * @param Table $table Table for $entity + * @param EntityInterface $entity Entity to copy (filter) + * @param array $related Related models to process + * @return array Array of filtered attributes + */ + + protected function filterMetadataForCopy( + \Cake\ORM\Table $table, + \Cake\Datasource\EntityInterface $entity, + array $related=[] + ): array { +// XXX There is overlap with Petitions::duplicateFilterEntityData and +// TableMetaTrait::filterMetadataFields (used mostly for UI stuff), +// should maybe refactor these. filterMetadata() is more based on the Petitions one +// See also CFM-442 + + $ret = []; + + $metaFields = [ + // Metadata fields start with the basic Cake metadata + 'id', + 'created', + 'modified', + // Add changelog metadata + 'actor_identifier', + 'deleted', + 'revision', + $entity->changelogAttributeName() + ]; + + // Handling parent keys is a bit complex for duplicating models, and we're probably + // going to need to know some context. For example, when we duplicate an Enrollment + // Flow we want to create a new Enrollment Flow and Enrollment Flow Step, but we + // want to Enrollment Flow Step to point to an existing Message Template. However + // when we duplicate an entire CO we also need to duplicate the Message Template. + // XXX For now we only support the first scenario, which we implement by removing + // primary keys, not all foreign keys. + + // Find the primary link for this entity. We want to keep co_id (presumably the + // top level primary link) but otherwise remove the link to allow Cake to rekey. + + $link = $table->findPrimaryLinkEntity($entity); + $linkKey = StringUtilities::entityToForeignKey($link); + + if($linkKey != 'co_id') { + $metaFields[] = $linkKey; + } + + // Now that we've figured out the metadata, walk the list of visible attributes + // and populate the ones that are defined and not metadata fields + + foreach($entity->getVisible() as $visible) { + if(!in_array($visible, $metaFields) + && isset($entity->$visible) + // Skip arrays, which are related models + && !is_array($entity->$visible)) { + $ret[$visible] = $entity->$visible; + } + } + + // Next handle related models (recursively). If the current entity is a Pluggable model + // we need to handle the related plugin data specially. + + foreach($related as $k => $v) { + if(is_int($k)) { + // $v is the model name (EnrollmentFlowSteps) + // $m is the lowercased model name (enrollment_flow_steps) + $m = Inflector::tableize($v); + // $m1 is the singular version (enrollment_flow_step) + $m1 = Inflector::singularize($m); + // $t is the Table for $v + if(!empty($entity->plugin) && StringUtilities::pluginModel($entity->plugin) == $v) { + // For pluggable models, get the plugin table from the entity configuration + $t = TableRegistry::getTableLocator()->get($entity->plugin); + } else { + $t = TableRegistry::getTableLocator()->get($v); + } + + if(is_array($entity->$m)) { + // HasMany + + foreach($entity->$m as $s) { + $ret[$m][] = $this->filterMetadataForDuplicate($t, $s); + } + } elseif(!empty($entity->$m1)) { + // HasOne + + $ret[$m1] = $this->filterMetadataForDuplicate($t, $entity->$m1); + } + } elseif(is_array($v)) { + // $k is the model name (EnrollmentFlowSteps) and $v is an array of related models + // $m is the lowercased model name (enrollment_flow_steps) + $m = Inflector::tableize($k); + // $m1 is the singular version (enrollment_flow_step) + $m1 = Inflector::singularize($m); + // $t is the Table for $k + if(!empty($entity->plugin) && StringUtilities::pluginModel($entity->plugin) == $v) { + // For pluggable models, get the plugin table from the entity configuration + $t = TableRegistry::getTableLocator()->get($entity->plugin); + } else { + $t = TableRegistry::getTableLocator()->get($k); + } + + if(is_array($entity->$m)) { + // HasMany + + foreach($entity->$m as $s) { + $ret[$m][] = $this->filterMetadataForDuplicate($t, $s, $v); + } + } elseif(!empty($entity->$m1)) { + // HasOne + + $ret[$m1] = $this->filterMetadataForDuplicate($t, $entity->$m1, $v); + } + } + } + + return $ret; + } + /** * Filter metadata fields. * diff --git a/app/src/Lib/Traits/ValidationTrait.php b/app/src/Lib/Traits/ValidationTrait.php index 8ef1b8d0f..d00f5ba9c 100644 --- a/app/src/Lib/Traits/ValidationTrait.php +++ b/app/src/Lib/Traits/ValidationTrait.php @@ -33,6 +33,8 @@ use Cake\Database\Schema\TableSchemaInterface; use Cake\ORM\TableRegistry; use Cake\Validation\Validator; +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; trait ValidationTrait { /** @@ -238,14 +240,28 @@ public function validateInput($value, array $context) { if(!empty($context['type'])) { switch($context['type']) { case 'html': - // We are accepting HTML input. We will mostly pass it all through and ensure - // properly sanitized output. However, we can do some very rudimentary checking for script tags. - // (An informational note should be placed below these fields as well.) - $lowercaseVal = strtolower($value); - if(str_contains($lowercaseVal, ' and