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 0976cc229..7b03d6f22 100644 --- a/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po +++ b/app/plugins/CoreEnroller/resources/locales/en_US/core_enroller.po @@ -140,6 +140,9 @@ msgstr "Telephone Number Type" msgid "field.EnrollmentAttributes.url_type_id" msgstr "URL Type" +msgid "field.IdentifierCollectors.type_id.desc" +msgstr "The collected Identifiers will be assigned this Type" + msgid "field.InvitationAccepters.invitation_validity" msgstr "Invitation Validity" @@ -161,6 +164,9 @@ msgstr "Petition Attributes recorded" msgid "result.basicattr.finalized" msgstr "Name, Email Address, and Person Role created during finalization" +msgid "result.IdentifierCollector.collected" +msgstr "Obtained login Identifier {0}" + msgid "result.InvitationAccepters.accepted" msgstr "Invitation Accepted at {0}" diff --git a/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php b/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php new file mode 100644 index 000000000..2b33b59c0 --- /dev/null +++ b/app/plugins/CoreEnroller/src/Controller/IdentifierCollectorsController.php @@ -0,0 +1,124 @@ + [ + 'IdentifierCollectors.id' => 'asc' + ] + ]; + + /** + * Dispatch an Enrollment Flow Step. + * + * @since COmanage Registry v5.1.0 + * @param string $id Invitation Accepter ID + */ + + public function dispatch(string $id) { + $petition = $this->getPetition(); + + $cfg = $this->IdentifierCollectors->get($id); + + // Because we're not in the "protected" web server application space, we need to read + // the username from the session (as set via TrafficController) rather than via getenv(). + // This also precludes us from using a variable other than $REMOTE_USER. + + $request = $this->getRequest(); + $session = $request->getSession(); + $username = $session->read('Auth.external.user'); + + $PetitionIdentifiers = TableRegistry::getTableLocator()->get('CoreEnroller.PetitionIdentifiers'); + + try { + $PetitionIdentifiers->record($petition->id, $cfg->enrollment_flow_step_id, $username); + + return $this->finishStep( + enrollmentFlowStepId: $cfg->enrollment_flow_step_id, + petitionId: $petition->id, + comment: __d('core_enroller', 'result.IdentifierCollector.collected', [$username]) + ); + } + catch(\Exception $e) { + $this->Flash->error($e->getMessage()); + } + } + + /** + * Display information about this Step. + * + * @since COmanage Registry v5.1.0 + * @param string $id Invitation Accepters ID + */ + + public function display(string $id) { + $petition = $this->getPetition(); + + $PetitionIdentifiers = TableRegistry::getTableLocator()->get('CoreEnroller.PetitionIdentifiers'); + + $this->set('vv_pi', $PetitionIdentifiers->find()->where(['petition_id' => $petition->id])->first()); + } + + /** + * Indicate whether this Controller will handle some or all authnz. + * + * @since COmanage Registry v5.1.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. + + // 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); + } +} \ No newline at end of file diff --git a/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php b/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php new file mode 100644 index 000000000..85e00897e --- /dev/null +++ b/app/plugins/CoreEnroller/src/Model/Entity/IdentifierCollector.php @@ -0,0 +1,49 @@ + + */ + protected $_accessible = [ + '*' => true, + 'id' => false, + 'slug' => false, + ]; +} diff --git a/app/plugins/CoreEnroller/src/Model/Entity/PetitionIdentifier.php b/app/plugins/CoreEnroller/src/Model/Entity/PetitionIdentifier.php new file mode 100644 index 000000000..7045282fe --- /dev/null +++ b/app/plugins/CoreEnroller/src/Model/Entity/PetitionIdentifier.php @@ -0,0 +1,49 @@ + + */ + protected $_accessible = [ + '*' => true, + 'id' => false, + 'slug' => false, + ]; +} diff --git a/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php b/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php new file mode 100644 index 000000000..895aeabff --- /dev/null +++ b/app/plugins/CoreEnroller/src/Model/Table/IdentifierCollectorsTable.php @@ -0,0 +1,163 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + $this->setTableType(\App\Lib\Enum\TableTypeEnum::Configuration); + + // Define associations + $this->belongsTo('EnrollmentFlowSteps'); + $this->belongsTo('Types'); + + // We intentionally don't hasMany PetitionIdentifiers since there should only be one + // collector per Petition, and the net result of not having the direct foreign key + // is that if an admin instantiates the plugin multiple times the second instance + // will refuse to do anything. + + $this->setDisplayField('id'); + + $this->setPrimaryLink('enrollment_flow_step_id'); + $this->setRequiresCO(true); + $this->setAllowLookupPrimaryLink(['dispatch', 'display']); + + $this->setAutoViewVars([ + 'types' => [ + 'type' => 'type', + 'attribute' => 'Identifiers.type' + ] + ]); + + $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 finalize the Petition. + * + * @since COmanage Registry v5.1.0 + * @param int $id Invitation Accepter ID + * @param Petition $petition Petition + * @return bool true on success + */ + + public function finalize(int $id, \App\Model\Entity\Petition $petition) { + $cfg = $this->get($id); + + // Pull the Identifier that was recorded + + $PetitionIdentifiers = TableRegistry::getTableLocator()->get('CoreEnroller.PetitionIdentifiers'); + + $pidentifier = $PetitionIdentifiers->find()->where(['petition_id' => $petition->id])->first(); + + if(!empty($pidentifier->identifier)) { + // Copy the Identifier to the Person record in accordance with the configuration + + $Identifiers = TableRegistry::getTableLocator()->get('Identifiers'); + + $id = $Identifiers->newEntity([ + 'person_id' => $petition->enrollee_person_id, + 'identifier' => $pidentifier->identifier, + 'type_id' => $cfg->type_id, + 'status' => SuspendableStatusEnum::Active, + 'login' => true + ]); + + $Identifiers->saveOrFail($id); + } + + return true; + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.1.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('type_id', [ + 'content' => ['rule' => 'isInteger'] + ]); + $validator->notEmptyString('type_id'); + + return $validator; + } +} diff --git a/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php b/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php index dfc844519..2fbdfe5fa 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/InvitationAcceptersTable.php @@ -138,7 +138,7 @@ public function prepare( // more requirements we should probably change this to do something else (eg: // fast forward to the next step). - if(!is_null($acceptance)) { + if(!is_null($acceptance->accepted)) { throw new \RuntimeException(__d('core_enroller', 'error.PetitionAcceptances.exists')); } @@ -146,10 +146,10 @@ public function prepare( } else { // Register a new, pending invitation - $acceptance = [ + $acceptance = $PetitionAcceptances->newEntity([ 'petition_id' => $petition->id, 'accepted' => null - ]; + ]); // If for some reason there is already an acceptance record // for this petition we'll basically reset it diff --git a/app/plugins/CoreEnroller/src/Model/Table/PetitionAcceptancesTable.php b/app/plugins/CoreEnroller/src/Model/Table/PetitionAcceptancesTable.php index 76a716cfd..d5eb08c65 100644 --- a/app/plugins/CoreEnroller/src/Model/Table/PetitionAcceptancesTable.php +++ b/app/plugins/CoreEnroller/src/Model/Table/PetitionAcceptancesTable.php @@ -79,7 +79,7 @@ public function initialize(array $config): void { // Actions that operate over a table (ie: do not require an $id) 'table' => [ 'add' => false, - 'index' => false //['platformAdmin', 'coAdmin'] + 'index' => false ] ]); } @@ -144,7 +144,7 @@ public function processReply(int $petitionId, int $enrollmentFlowStepId, bool $a $this->Petitions->PetitionHistoryRecords->record( petitionId: $petitionId, enrollmentFlowStepId: $enrollmentFlowStepId, - action: PetitionActionEnum::Finalized, + action: PetitionActionEnum::StatusUpdated, comment: __d('core_enroller', $accepted ? 'result.accept.accepted' : 'result.accept.declined') // We don't have $actorPersonId yet... // ?int $actorPersonId=null diff --git a/app/plugins/CoreEnroller/src/Model/Table/PetitionIdentifiersTable.php b/app/plugins/CoreEnroller/src/Model/Table/PetitionIdentifiersTable.php new file mode 100644 index 000000000..e9d529c2e --- /dev/null +++ b/app/plugins/CoreEnroller/src/Model/Table/PetitionIdentifiersTable.php @@ -0,0 +1,139 @@ +addBehavior('Changelog'); + $this->addBehavior('Log'); + $this->addBehavior('Timestamp'); + + $this->setTableType(\App\Lib\Enum\TableTypeEnum::Artifact); + + // Define associations + $this->belongsTo('Petitions'); + + $this->setDisplayField('identifier'); + + $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 Identifier. + * + * @since COmanage Registry v5.1.0 + * @param int $petitionId Petition ID + * @param int $enrollmentFlowStepId Enrollment Flow Step ID + * @param string $identifier Login Identifier + */ + + public function record(int $petitionId, int $enrollmentFlowStepId, string $identifier) { + // Record the Identifier. We use upsert since at least initially we only support + // one Identifier per Petition. + + $pi = [ + 'petition_id' => $petitionId, + 'identifier' => $identifier + ]; + + $this->upsertOrFail($pi, ['petition_id' => $petitionId]); + + // Record PetitionHistory + + $this->Petitions->PetitionHistoryRecords->record( + petitionId: $petitionId, + enrollmentFlowStepId: $enrollmentFlowStepId, + action: PetitionActionEnum::AttributesUpdated, + comment: __d('core_enroller', 'result.IdentifierCollector.collected', [$identifier]) +// We don't have $actorPersonId yet... +// ?int $actorPersonId=null + ); + } + + /** + * Set validation rules. + * + * @since COmanage Registry v5.1.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'); + + $this->registerStringValidation($validator, $schema, 'identifier', true); + + return $validator; + } +} diff --git a/app/plugins/CoreEnroller/src/config/plugin.json b/app/plugins/CoreEnroller/src/config/plugin.json index 868f55049..158bf2334 100644 --- a/app/plugins/CoreEnroller/src/config/plugin.json +++ b/app/plugins/CoreEnroller/src/config/plugin.json @@ -3,6 +3,8 @@ "enroller": [ "AttributeCollectors", "BasicAttributeCollectors", + "EmailVerifiers", + "IdentifierCollectors", "InvitationAccepters" ] }, @@ -52,6 +54,16 @@ "petition_basic_attribute_sets_i2": { "columns": [ "basic_attribute_collector_id" ] } } }, + "email_verifiers": { + "columns": { + "id": {}, + "enrollment_flow_step_id": {}, + "mode": { "type": "string", "size": 2 } + }, + "indexes": { + "email_verifiers_i1": { "columns": [ "enrollment_flow_step_id" ] } + } + }, "enrollment_attributes": { "columns": { "id": {}, @@ -77,6 +89,17 @@ "enrollment_attributes_i1": { "columns": [ "attribute_collector_id" ] } } }, + "identifier_collectors": { + "columns": { + "id": {}, + "enrollment_flow_step_id": {}, + "type_id": { "notnull": false, "comment": "Skeletal row needs to be created without type" } + }, + "indexes": { + "identifier_collectors_i1": { "columns": [ "enrollment_flow_step_id" ] }, + "identifier_collectors_i2": { "needed": false, "columns": [ "type_id" ] } + } + }, "invitation_accepters": { "columns": { "id": {}, @@ -109,6 +132,28 @@ "petition_attributes_i1": { "columns": [ "petition_id" ] }, "petition_attributes_i2": { "needed": false, "columns": [ "enrollment_attribute_id" ] } } + }, + "petition_identifiers": { + "columns": { + "id": {}, + "petition_id": {}, + "identifier": { "type": "string", "size": 512 } + }, + "indexes": { + "petition_identifiers_i1": { "columns": [ "petition_id" ] } + } + }, + "petition_verifications": { + "columns": { + "id": {}, + "petition_id": {}, + "mail": {}, + "verification_id": {} + }, + "indexes": { + "petition_verifications_i1": { "columns": [ "petition_id" ] }, + "petition_verifications_i2": { "needed": false, "columns": [ "verification_id" ] } + } } } } diff --git a/app/plugins/CoreEnroller/templates/IdentifierCollectors/display.php b/app/plugins/CoreEnroller/templates/IdentifierCollectors/display.php new file mode 100644 index 000000000..30359a605 --- /dev/null +++ b/app/plugins/CoreEnroller/templates/IdentifierCollectors/display.php @@ -0,0 +1,4 @@ +identifier)) { + print __d('core_enroller', 'result.IdentifierCollector.collected', [$vv_pi->identifier]); +} diff --git a/app/plugins/CoreEnroller/templates/IdentifierCollectors/fields.inc b/app/plugins/CoreEnroller/templates/IdentifierCollectors/fields.inc new file mode 100644 index 000000000..d2eaa73be --- /dev/null +++ b/app/plugins/CoreEnroller/templates/IdentifierCollectors/fields.inc @@ -0,0 +1,33 @@ +element('form/listItem', [ + 'arguments' => ['fieldName' => 'type_id'] + ]); +}