From 880184b1e0036bbfa3894a3e9d5681da8c51a6d5 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 22 Jul 2024 19:36:51 +0300 Subject: [PATCH 1/8] Fix the parent breadcrumb of an External Identity Source Record --- .../src/Model/Table/FileSourcesTable.php | 6 +- app/src/Controller/AppController.php | 371 ++++++++++-------- .../Component/BreadcrumbComponent.php | 10 +- .../ExtIdentitySourceRecordsController.php | 16 +- app/src/Lib/Traits/PrimaryLinkTrait.php | 2 +- .../Table/ExtIdentitySourceRecordsTable.php | 4 +- .../Model/Table/ExternalIdentitiesTable.php | 1 + 7 files changed, 242 insertions(+), 168 deletions(-) diff --git a/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php b/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php index 6861ffc63..c51a21307 100644 --- a/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php +++ b/app/availableplugins/FileConnector/src/Model/Table/FileSourcesTable.php @@ -449,9 +449,11 @@ public function search( while(($data = fgetcsv($handle)) !== false) { // strtolower, previous behavior was full string only so dupe that - $match = array_search(strtolower($searchAttrs['q']), array_map('strtolower', $data)); + $match = collection($data) + ->map(fn($value, $key) => strtolower($value ?? '')) + ->filter(fn($item, $key) => strtolower($searchAttrs['q']) === $item); - if($match !== false) { + if($match->count() > 0) { // $match will be the CSV column that matched, but for now we ignore that // since we just need to know that the row matched somewhere. Note the first // column is always the SORID. diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index e190ff1ab..ade7ce791 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -44,6 +44,7 @@ use Cake\ORM\TableRegistry; use Cake\Utility\Hash; use InvalidArgumentException; +use JsonSchema\Iterator\ObjectIterator; class AppController extends Controller { use \App\Lib\Traits\LabeledLogTrait; @@ -187,12 +188,12 @@ public function getCO(): ?\App\Model\Entity\Co { // We'll return null if no CO, since some contexts may need to know that return $this->cur_co; } - + /** * Get the current CO ID. * + * @return int|null CO ID, or null * @since COmanage Registry v5.0.0 - * @return int CO ID, or null */ public function getCOID(): ?int { @@ -201,6 +202,178 @@ public function getCOID(): ?int { return $cur_co ? $cur_co->id : null; } + /** + * @param string $potentialPrimaryLink + * + * @return Object|bool + * @since COmanage Registry v5.0.0 + */ + + protected function primaryLinkOnGet(string $potentialPrimaryLink): Object|bool + { + // $this->name = Models + $modelsName = $this->name; + + // If this action allows unkeyed, asserted primary link IDs, check the query + // string (e.g.: 'add' or 'index' allow matchgrid_id to be passed in) + $actionParam = $this->request->getParam('action'); + $allowsUnkeyed = $this->$modelsName->allowUnkeyedPrimaryLink($actionParam); + $allowsLookup = $this->$modelsName->allowLookupPrimaryLink($actionParam); + $param = (int)$this->request->getParam('pass.0'); + + if($allowsUnkeyed) { + $query = $this->request->getQuery(); + if($query) { + $this->cur_pl->value = $this->request->getQuery($potentialPrimaryLink); + return false; + } + } + + if(!$allowsLookup || empty($param)) { + return false; + } + + return $this->$modelsName->findPrimaryLink($param); + } + + /** + * @param string $potentialPrimaryLink + * + * @return Object|bool + * @since COmanage Registry v5.0.0 + * + */ + + protected function primaryLinkOnPost(string $potentialPrimaryLink): Object|bool + { + // $this->name = Models + $modelsName = $this->name; + + // Post = add, where we can have a list of objects and nothing in /objects/{id} + // We don't support different primary links across objects, so we throw an error + // if different parent keys are provided. + + $linkValue = null; + + // Data in API format + $reqData = $this->request->getData($modelsName); + + if(!$reqData + // Don't create $reqData if the POST data is also empty + && !empty($this->request->getData())) { + // Data in POST format + $reqData[] = $this->request->getData(); + } + + if(!empty($reqData)) { + foreach($reqData as $rec) { + if(!empty($rec[$potentialPrimaryLink])) { + if(!$linkValue) { + // This is the first record we've seen, use this primary link value + $linkValue = $rec[$potentialPrimaryLink]; + } elseif($linkValue != $rec[$potentialPrimaryLink]) { + // We don't support multiple records with different parents + throw new \InvalidArgumentException(__d('error', 'primary_link.mismatch')); + } + } + + $this->cur_pl->value = $linkValue; + } + } + + // If we didn't find the primary link in the submitted form or API + // request, it might be available via the URL. + + if(!$linkValue + && $this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { + // Try to map the requested object ID (this is probably a delete, so no attribute in post body) + $param = (int)$this->request->getParam('pass.0'); + + if(!empty($param)) { + return $this->$modelsName->findPrimaryLink($param); + } + } + + return false; + } + + /** + * @return Object|bool + * @since COmanage Registry v5.0.0 + * + */ + + protected function primaryLinkOnPut(): Object|bool + { + // $this->name = Models + $modelsName = $this->name; + $param = (int)$this->request->getParam('pass.0'); + + // Put = edit, so we should look up the parent ID via the object itself + if ( + empty($param) + || + !$this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action')) + ) { + return false; + } + + return $this->$modelsName->findPrimaryLink($param); + } + + /** + * @return void + */ + protected function primaryLinkLookup(): void + { + // $this->name = Models + $modelsName = $this->name; + $availablePrimaryLinks = $this->$modelsName->getPrimaryLinks(); + + foreach($availablePrimaryLinks as $potentialPrimaryLink) { + // $potentialPrimaryLink will be something like 'attribute_collector_id' + // $potentialPrimaryLinkTable will be something like 'CoreEnroller.AttributeCollectors' + $potentialPrimaryLinkTable = $this->$modelsName->getPrimaryLinkTableName($potentialPrimaryLink); + + $cur = match(true) { + $this->request->is('get') => $this->primaryLinkOnGet($potentialPrimaryLink), + ($this->request->is('post') && $this->request->getParam('action') != 'delete') => $this->primaryLinkOnPost($potentialPrimaryLink), + ($this->request->is('put') || $this->request->getParam('action') == 'delete') => $this->primaryLinkOnPut(), + default => false, + }; + + if($cur !== false) { + $this->cur_pl = $cur; + $this->set('vv_primary_link', $this->cur_pl->attr); + break; + } + + if(!empty($this->cur_pl->value)) { + // For looking up values in records here, we want only the attribute + // itself and not the plugin name (used for hacky notation by + // PrimaryLinkTrait::setPrimaryLink(). Note this is a field and not + // a model, but pluginModel() gets us the bit we need. + + // Store the plugin for possible later reference. + $potentialPlugin = str_contains($potentialPrimaryLinkTable, '.') + ? StringUtilities::pluginPlugin($potentialPrimaryLinkTable) + : null; + + // We found a populated primary link. Store the attribute and break the loop. + $this->cur_pl->attr = $potentialPrimaryLink; + if($potentialPlugin) { + $this->cur_pl->plugin = $potentialPlugin; + } + $this->set('vv_primary_link', $this->cur_pl->attr); + break; + } + } + + if(empty($this->cur_pl->value) && !$this->$modelsName->allowEmptyPrimaryLink($this->request->getParam('action'))) { + throw new \RuntimeException(__d('error', 'primary_link')); + } + } + /** * Obtain information about the Standard Object's Primary Link, if set. * The $vv_primary_link view variable is also set. @@ -210,173 +383,57 @@ public function getCOID(): ?int { * @return object Object holding the primary link attribute, and optionally its value * @throws \RuntimeException */ - + public function getPrimaryLink(bool $lookup=false) { // Did we already figure this out? (But only if $lookup) if($lookup && isset($this->cur_pl->value)) { return $this->cur_pl; } - + // $this->name = Models $modelsName = $this->name; - // $modelName = Model - $modelName = \Cake\Utility\Inflector::singularize($modelsName); $this->cur_pl = new \stdClass(); - + + if(!(method_exists($this->$modelsName, 'getPrimaryLinks') + && $this->$modelsName->getPrimaryLinks()) + ) { + return $this->cur_pl; + } + // PrimaryLinkTrait - if(method_exists($this->$modelsName, "getPrimaryLinks") - && $this->$modelsName->getPrimaryLinks()) { - // Some models, in particular MVEAs, can have multiple potential primary - // links. In these cases, only one primary link is valid at a time, so we - // have to look through the available primary links and find one. - - $availablePrimaryLinks = $this->$modelsName->getPrimaryLinks(); - - if($lookup) { - foreach($availablePrimaryLinks as $potentialPrimaryLink) { - // $potentialPrimaryLink will be something like 'attribute_collector_id' - // $potentialPrimaryLinkTable will be something like 'CoreEnroller.AttributeCollectors' - $potentialPrimaryLinkTable = $this->$modelsName->getPrimaryLinkTableName($potentialPrimaryLink); - $potentialPlugin = null; - - // Try to find a value - - if(strstr($potentialPrimaryLinkTable, '.')) { - // For looking up values in records here, we want only the attribute - // itself and not the plugin name (used for hacky notation by - // PrimaryLinkTrait::setPrimaryLink(). Note this is a field and not - // a model, but pluginModel() gets us the bit we need. - - // Store the plugin for possible later reference. - $potentialPlugin = StringUtilities::pluginPlugin($potentialPrimaryLinkTable); - } - - if($this->request->is('get')) { - // If this action allows unkeyed, asserted primary link IDs, check the query - // string (eg: 'add' or 'index' allow matchgrid_id to be passed in) - if($this->$modelsName->allowUnkeyedPrimaryLink($this->request->getParam('action')) - && $this->request->getQuery()) { - $this->cur_pl->value = $this->request->getQuery($potentialPrimaryLink); - } elseif($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $this->cur_pl = $this->$modelsName->findPrimaryLink($param); - // Break the loop here since we also have the link attribute, - // which might not be $potentialPrimaryLink - $this->set('vv_primary_link', $this->cur_pl->attr); - break; - } - } - } elseif($this->request->is('post') && $this->request->getParam('action') != 'delete') { - // Post = add, where we can have a list of objects and nothing in /objects/{id} - // We don't support different primary links across objects, so we throw an error - // if different parent keys are provided. - - $linkValue = null; - - // Data in API format - $reqData = $this->request->getData($modelsName); - - if(!$reqData - // Don't create $reqData if the POST data is also empty - && !empty($this->request->getData())) { - // Data in POST format - $reqData[] = $this->request->getData(); - } - - if(!empty($reqData)) { - foreach($reqData as $rec) { - if(!empty($rec[$potentialPrimaryLink])) { - if(!$linkValue) { - // This is the first record we've seen, use this primary link value - $linkValue = $rec[$potentialPrimaryLink]; - } elseif($linkValue != $rec[$potentialPrimaryLink]) { - // We don't support multiple records with different parents - throw new \InvalidArgumentException(__d('error', 'primary_link.mismatch')); - } - } - - $this->cur_pl->value = $linkValue; - } - } - - // If we didn't find the primary link in the submitted form or API - // request, it might be available via the URL. - - if(!$linkValue - && $this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID (this is probably a delete, so no attribute in post body) - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $this->cur_pl = $this->$modelsName->findPrimaryLink($param); - // Break the loop here since we also have the link attribute, - // which might not be $potentialPrimaryLink - $this->set('vv_primary_link', $this->cur_pl->attr); - break; - } - } - } elseif($this->request->is('put') || $this->request->getParam('action') == 'delete') { - // Put = edit, so we should look up the parent ID via the object itself - if($this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID (this is probably a delete, so no attribute in post body) - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - $this->cur_pl = $this->$modelsName->findPrimaryLink($param); - // Break the loop here since we also have the link attribute, - // which might not be $potentialPrimaryLink - $this->set('vv_primary_link', $this->cur_pl->attr); - break; - } - } - } - - if(!empty($this->cur_pl->value)) { - // We found a populated primary link. Store the attribute and break the loop. - $this->cur_pl->attr = $potentialPrimaryLink; - if($potentialPlugin) { - $this->cur_pl->plugin = $potentialPlugin; - } - $this->set('vv_primary_link', $this->cur_pl->attr); - break; - } - } - - if(empty($this->cur_pl->value) - && !$this->$modelsName->allowEmptyPrimaryLink($this->request->getParam('action'))) { - throw new \RuntimeException(__d('error', 'primary_link')); + // Some models, in particular MVEAs, can have multiple potential primary + // links. In these cases, only one primary link is valid at a time, so we + // have to look through the available primary links and find one. + + if($lookup) { + $this->primaryLinkLookup(); + } + + if(!empty($this->cur_pl->value)) { + // Look up the link value to find the related entity + + $linkTableName = $this->$modelsName->getPrimaryLinkTableName($this->cur_pl->attr); + $linkTable = $this->getTableLocator()->get($linkTableName); + + $this->set('vv_primary_link_model', $linkTableName); + + try { + $plObj = $linkTable->findById($this->cur_pl->value)->firstOrFail(); + + $this->set('vv_primary_link_obj', $plObj); + + // While we're here, note the CO since we'll probably need it soon + if(!empty($plObj->co_id)) { + $this->cur_pl->co_id = $plObj->co_id; + } elseif(method_exists($linkTable, 'findCoForRecord')) { + $this->cur_pl->co_id = $linkTable->findCoForRecord((int)$this->cur_pl->value); } } - - if(!empty($this->cur_pl->value)) { - // Look up the link value to find the related entity - - $linkTableName = $this->$modelsName->getPrimaryLinkTableName($this->cur_pl->attr); - $linkTable = $this->getTableLocator()->get($linkTableName); - - $this->set('vv_primary_link_model', $linkTableName); - - try { - $plObj = $linkTable->findById($this->cur_pl->value)->firstOrFail(); - - $this->set('vv_primary_link_obj', $plObj); - - // While we're here, note the CO since we'll probably need it soon - if(!empty($plObj->co_id)) { - $this->cur_pl->co_id = $plObj->co_id; - } elseif(method_exists($linkTable, "findCoForRecord")) { - $this->cur_pl->co_id = $linkTable->findCoForRecord((int)$this->cur_pl->value); - } - } - catch(RecordNotFoundException $e) { - $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkTableName); - // Mask this with a generic UnauthorizedException - throw new UnauthorizedException(__d('error', 'perm')); - } + catch(RecordNotFoundException $e) { + $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkTableName); + // Mask this with a generic UnauthorizedException + throw new UnauthorizedException(__d('error', 'perm')); } } diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index ec500e4af..16a3a99d7 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -57,11 +57,11 @@ class BreadcrumbComponent extends Component // Don't render the parent links protected $skipParentPaths = []; // Inject parent links (these render before the index link, if set) - // The parent links are constructed as part of the injectPrimaryLink function. This in the StandardController as well - // as in the StandardPluginController, MVEAController, ProvisioningHistoryRecordController, etc. These controllers are - // a descendant from the StandardController we will calculate the Parents twice. In order to avoid duplicates the - // injectParents table has to be an associative array. The uniqueness of the key will preserve the uniqueness of the parent - // path while the order of firing will create the correct breadcumb path order + // The parent links are constructed as part of the injectPrimaryLink function, as well as in the StandardController and + // in the StandardPluginController, MVEAController, ProvisioningHistoryRecordController, etc. These controllers are + // a descendant from the StandardController we will calculate the Parents twice. To avoid duplicates, the + // injectParents table has to be an associative array. The uniqueness of the key will preserve the uniqueness of + // the parent path while the order of firing will create the correct breadcrumb path order protected $injectParents = []; // Inject title links (immediately before the title breadcrumb) protected $injectTitleLinks = []; diff --git a/app/src/Controller/ExtIdentitySourceRecordsController.php b/app/src/Controller/ExtIdentitySourceRecordsController.php index 5f07920d1..7f24d4fd1 100644 --- a/app/src/Controller/ExtIdentitySourceRecordsController.php +++ b/app/src/Controller/ExtIdentitySourceRecordsController.php @@ -33,10 +33,24 @@ use Cake\Log\Log; use Cake\ORM\TableRegistry; -class ExtIdentitySourceRecordsController extends StandardController { +class ExtIdentitySourceRecordsController extends MVEAController { public $paginate = [ 'order' => [ 'ExtIdentitySourceRecords.id' => 'asc' ] ]; + + /** + * Perform Controller initialization. + * + * @since COmanage Registry v5.0.0 + */ + + public function initialize(): void + { + parent::initialize(); + + // Configure breadcrumb rendering + $this->Breadcrumb->skipParents(['/^\/ext-identity-source-records/']); + } } \ No newline at end of file diff --git a/app/src/Lib/Traits/PrimaryLinkTrait.php b/app/src/Lib/Traits/PrimaryLinkTrait.php index f0360853d..41e4751e6 100644 --- a/app/src/Lib/Traits/PrimaryLinkTrait.php +++ b/app/src/Lib/Traits/PrimaryLinkTrait.php @@ -464,7 +464,7 @@ public function setCurCoId(int $coId) { } /** - * Set the primary link attribute. Several formats are acceepted: + * Set the primary link attribute. Several formats are accepted: * * 1: ('person_id'): Primary link is to People * 2: (['person_id', 'group_id']): Primary link can be to People _or_ Groups diff --git a/app/src/Model/Table/ExtIdentitySourceRecordsTable.php b/app/src/Model/Table/ExtIdentitySourceRecordsTable.php index b14d55211..7a81c2aac 100644 --- a/app/src/Model/Table/ExtIdentitySourceRecordsTable.php +++ b/app/src/Model/Table/ExtIdentitySourceRecordsTable.php @@ -71,11 +71,11 @@ public function initialize(array $config): void { $this->setDisplayField('source_key'); - $this->setPrimaryLink(['external_identity_source_id']); + $this->setPrimaryLink(['external_identity_source_id', 'external_identity_id']); $this->setRequiresCO(true); // These are required for the link to work from the Artifacts page - $this->setAllowUnkeyedPrimaryCO(['index']); + $this->setAllowUnkeyedPrimaryCO(['index', 'view']); $this->setAllowEmptyPrimaryLink(['index']); $this->setIndexContains([ diff --git a/app/src/Model/Table/ExternalIdentitiesTable.php b/app/src/Model/Table/ExternalIdentitiesTable.php index 62a7cf945..9f945a039 100644 --- a/app/src/Model/Table/ExternalIdentitiesTable.php +++ b/app/src/Model/Table/ExternalIdentitiesTable.php @@ -117,6 +117,7 @@ public function initialize(array $config): void { 'Addresses', 'AdHocAttributes', 'EmailAddresses', + 'ExtIdentitySourceRecords' => ['ExternalIdentitySources'], 'Identifiers', 'Names', //'ExternalIdentityRoles', From 882f870b58a350e90a2ac1ee5cd2ded3af8dd6be Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Mon, 22 Jul 2024 20:37:59 +0300 Subject: [PATCH 2/8] View ExternalIdentitySourceRecord render in Modal --- .../Model/Table/ExtIdentitySourceRecordsTable.php | 14 +++++++++++++- app/templates/ExternalIdentities/fields-nav.inc | 4 ++++ .../ExternalIdentitySources/retrieve.php | 4 ++++ app/templates/Standard/add-edit-view.php | 15 +++++++-------- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/src/Model/Table/ExtIdentitySourceRecordsTable.php b/app/src/Model/Table/ExtIdentitySourceRecordsTable.php index 7a81c2aac..c8356f2f4 100644 --- a/app/src/Model/Table/ExtIdentitySourceRecordsTable.php +++ b/app/src/Model/Table/ExtIdentitySourceRecordsTable.php @@ -49,7 +49,19 @@ class ExtIdentitySourceRecordsTable extends Table { use \App\Lib\Traits\SearchFilterTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; - + + /** + * Provide the default layout + * + * @since COmanage Registry v5.0.0 + * @return string Type of redirect + */ + public function getLayout(string $action = ''): string { + return match($action) { + default => 'iframe' + }; + } + /** * Perform Cake Model initialization. * diff --git a/app/templates/ExternalIdentities/fields-nav.inc b/app/templates/ExternalIdentities/fields-nav.inc index 628cb7353..1ac066f34 100644 --- a/app/templates/ExternalIdentities/fields-nav.inc +++ b/app/templates/ExternalIdentities/fields-nav.inc @@ -58,6 +58,10 @@ $topLinks = [ [ 'icon' => 'visibility', 'order' => 'Default', + 'class' => 'cm-modal-link nospin', // launch this in a modal + 'dataAttrs' => [ + ['data-cm-modal-title',__d('controller', 'ExtIdentitySourceRecords', 1)] + ], 'label' => __d('operation', 'view.a', [__d('controller', 'ExtIdentitySourceRecords', 1)]), 'link' => [ 'controller' => 'ext_identity_source_records', diff --git a/app/templates/ExternalIdentitySources/retrieve.php b/app/templates/ExternalIdentitySources/retrieve.php index af4155e38..b9b61a094 100644 --- a/app/templates/ExternalIdentitySources/retrieve.php +++ b/app/templates/ExternalIdentitySources/retrieve.php @@ -71,6 +71,10 @@ $action_args['vv_actions'][] = [ 'order' => 3, 'icon' => 'visibility', + 'class' => 'cm-modal-link nospin', // launch this in a modal + 'dataAttrs' => [ + ['data-cm-modal-title',__d('controller', 'ExtIdentitySourceRecords', 1)] + ], 'url' => [ 'controller' => 'ext-identity-source-records', 'action' => 'view', diff --git a/app/templates/Standard/add-edit-view.php b/app/templates/Standard/add-edit-view.php index bce04253c..fd4386874 100644 --- a/app/templates/Standard/add-edit-view.php +++ b/app/templates/Standard/add-edit-view.php @@ -134,14 +134,13 @@ } if($perm) { - $action_args['vv_actions'][] = [ - 'order' => $this->Menu->getMenuOrder($t['order']), - 'icon' => $this->Menu->getMenuIcon($t['icon']), - 'url' => $t['link'], - 'label' => $t['label'], - 'class' => !empty($t['class']) ? $t['class'] : '', - 'confirm' => !empty($t['confirm']) ? $t['confirm'] : [] - ]; + $action_args['vv_actions'][] = $t; + $key = array_key_last($action_args['vv_actions']); + $action_args['vv_actions'][$key]['order'] = $this->Menu->getMenuOrder($t['order']); + $action_args['vv_actions'][$key]['icon'] = $this->Menu->getMenuIcon($t['icon']); + $action_args['vv_actions'][$key]['url'] = $t['link'] ?? ''; + $action_args['vv_actions'][$key]['class'] = $t['class'] ?? ''; + $action_args['vv_actions'][$key]['confirm'] = $t['confirm'] ?? ''; } } From 381ec105a6d2fd451800942e7247a3836270e123 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 23 Jul 2024 13:24:54 +0300 Subject: [PATCH 3/8] Refactor/Simplify Primary Link Calculation --- app/src/Controller/AppController.php | 179 +++++++++++++-------------- 1 file changed, 87 insertions(+), 92 deletions(-) diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index ade7ce791..51ff92c50 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -224,8 +224,7 @@ protected function primaryLinkOnGet(string $potentialPrimaryLink): Object|bool if($allowsUnkeyed) { $query = $this->request->getQuery(); if($query) { - $this->cur_pl->value = $this->request->getQuery($potentialPrimaryLink); - return false; + return $this->populatedPrimaryLink($potentialPrimaryLink); } } @@ -252,52 +251,26 @@ protected function primaryLinkOnPost(string $potentialPrimaryLink): Object|bool // Post = add, where we can have a list of objects and nothing in /objects/{id} // We don't support different primary links across objects, so we throw an error // if different parent keys are provided. - - $linkValue = null; - - // Data in API format - $reqData = $this->request->getData($modelsName); - - if(!$reqData - // Don't create $reqData if the POST data is also empty - && !empty($this->request->getData())) { - // Data in POST format - $reqData[] = $this->request->getData(); + // Data in API format | Data in POST format + $reqData = $this->request->getData($modelsName) ?? $this->request->getData() ?? []; + $potentialPrimaryLinkRecords = collection($reqData)->filter(fn($value, $key) => $key === $potentialPrimaryLink); + if($potentialPrimaryLinkRecords->count() > 1) { + // We don't support multiple records with different parents + throw new \InvalidArgumentException(__d('error', 'primary_link.mismatch')); } - - if(!empty($reqData)) { - foreach($reqData as $rec) { - if(!empty($rec[$potentialPrimaryLink])) { - if(!$linkValue) { - // This is the first record we've seen, use this primary link value - $linkValue = $rec[$potentialPrimaryLink]; - } elseif($linkValue != $rec[$potentialPrimaryLink]) { - // We don't support multiple records with different parents - throw new \InvalidArgumentException(__d('error', 'primary_link.mismatch')); - } - } - - $this->cur_pl->value = $linkValue; - } + if($potentialPrimaryLinkRecords->count() === 1) { + return $this->populatedPrimaryLink($potentialPrimaryLink); } // If we didn't find the primary link in the submitted form or API // request, it might be available via the URL. - if(!$linkValue - && $this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action'))) { - // Try to map the requested object ID (this is probably a delete, so no attribute in post body) - $param = (int)$this->request->getParam('pass.0'); - - if(!empty($param)) { - return $this->$modelsName->findPrimaryLink($param); - } - } - - return false; + return $this->primaryLinkOnPut(); } /** + * Primary link available via the URL. + * * @return Object|bool * @since COmanage Registry v5.0.0 * @@ -322,7 +295,45 @@ protected function primaryLinkOnPut(): Object|bool } /** - * @return void + * @param string $potentialPrimaryLink + * + * @return Object|\stdClass + */ + protected function populatedPrimaryLink(string $potentialPrimaryLink): Object + { + // $this->name = Models + $modelsName = $this->name; + // $potentialPrimaryLink will be something like 'attribute_collector_id' + // $potentialPrimaryLinkTable will be something like 'CoreEnroller.AttributeCollectors' + $potentialPrimaryLinkTable = $this->$modelsName->getPrimaryLinkTableName($potentialPrimaryLink); + + // For looking up values in records here, we want only the attribute + // itself and not the plugin name (used for hacky notation by + // PrimaryLinkTrait::setPrimaryLink(). Note this is a field and not + // a model, but pluginModel() gets us the bit we need. + + // Store the plugin for possible later reference. + $potentialPlugin = str_contains($potentialPrimaryLinkTable, '.') + ? StringUtilities::pluginPlugin($potentialPrimaryLinkTable) + : null; + + $cur = new \stdClass(); + $cur->value = $this->request->getQuery($potentialPrimaryLink); + // We found a populated primary link. Store the attribute and break the loop. + $cur->attr = $potentialPrimaryLink; + if($potentialPlugin) { + $cur->plugin = $potentialPlugin; + } + + return $cur; + } + + + /** + * Perform primary link lookup. + * + * @throws \RuntimeException Exception thrown if a primary link is empty and it is not allowed + * @since Symfony v7.0.3 */ protected function primaryLinkLookup(): void { @@ -330,11 +341,8 @@ protected function primaryLinkLookup(): void $modelsName = $this->name; $availablePrimaryLinks = $this->$modelsName->getPrimaryLinks(); + // Iterate over all the potential primary links and pick the appropriate one foreach($availablePrimaryLinks as $potentialPrimaryLink) { - // $potentialPrimaryLink will be something like 'attribute_collector_id' - // $potentialPrimaryLinkTable will be something like 'CoreEnroller.AttributeCollectors' - $potentialPrimaryLinkTable = $this->$modelsName->getPrimaryLinkTableName($potentialPrimaryLink); - $cur = match(true) { $this->request->is('get') => $this->primaryLinkOnGet($potentialPrimaryLink), ($this->request->is('post') && $this->request->getParam('action') != 'delete') => $this->primaryLinkOnPost($potentialPrimaryLink), @@ -342,40 +350,23 @@ protected function primaryLinkLookup(): void default => false, }; - if($cur !== false) { + if($cur !== false && $cur->value !== null) { $this->cur_pl = $cur; $this->set('vv_primary_link', $this->cur_pl->attr); + // Exit the for loop break; } + } // foreach - if(!empty($this->cur_pl->value)) { - // For looking up values in records here, we want only the attribute - // itself and not the plugin name (used for hacky notation by - // PrimaryLinkTrait::setPrimaryLink(). Note this is a field and not - // a model, but pluginModel() gets us the bit we need. - - // Store the plugin for possible later reference. - $potentialPlugin = str_contains($potentialPrimaryLinkTable, '.') - ? StringUtilities::pluginPlugin($potentialPrimaryLinkTable) - : null; - - // We found a populated primary link. Store the attribute and break the loop. - $this->cur_pl->attr = $potentialPrimaryLink; - if($potentialPlugin) { - $this->cur_pl->plugin = $potentialPlugin; - } - $this->set('vv_primary_link', $this->cur_pl->attr); - break; - } - } - - if(empty($this->cur_pl->value) && !$this->$modelsName->allowEmptyPrimaryLink($this->request->getParam('action'))) { + // At the end we need to have a Primary Link + if(empty($this->cur_pl->value) + && !$this->$modelsName->allowEmptyPrimaryLink($this->request->getParam('action'))) { throw new \RuntimeException(__d('error', 'primary_link')); } } /** - * Obtain information about the Standard Object's Primary Link, if set. + * Collect information about the Standard Object's Primary Link, if set. * The $vv_primary_link view variable is also set. * * @since COmanage Registry v5.0.0 @@ -384,7 +375,8 @@ protected function primaryLinkLookup(): void * @throws \RuntimeException */ - public function getPrimaryLink(bool $lookup=false) { + public function getPrimaryLink(bool $lookup=false): Object + { // Did we already figure this out? (But only if $lookup) if($lookup && isset($this->cur_pl->value)) { return $this->cur_pl; @@ -410,42 +402,45 @@ public function getPrimaryLink(bool $lookup=false) { $this->primaryLinkLookup(); } - if(!empty($this->cur_pl->value)) { - // Look up the link value to find the related entity + if(empty($this->cur_pl->value)) { + return $this->cur_pl; + } - $linkTableName = $this->$modelsName->getPrimaryLinkTableName($this->cur_pl->attr); - $linkTable = $this->getTableLocator()->get($linkTableName); + // Look up the link value to find the related entity - $this->set('vv_primary_link_model', $linkTableName); + $linkTableName = $this->$modelsName->getPrimaryLinkTableName($this->cur_pl->attr); + $linkTable = $this->getTableLocator()->get($linkTableName); - try { - $plObj = $linkTable->findById($this->cur_pl->value)->firstOrFail(); + $this->set('vv_primary_link_model', $linkTableName); - $this->set('vv_primary_link_obj', $plObj); + try { + $plObj = $linkTable->findById($this->cur_pl->value)->firstOrFail(); - // While we're here, note the CO since we'll probably need it soon - if(!empty($plObj->co_id)) { - $this->cur_pl->co_id = $plObj->co_id; - } elseif(method_exists($linkTable, 'findCoForRecord')) { - $this->cur_pl->co_id = $linkTable->findCoForRecord((int)$this->cur_pl->value); - } - } - catch(RecordNotFoundException $e) { - $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkTableName); - // Mask this with a generic UnauthorizedException - throw new UnauthorizedException(__d('error', 'perm')); + $this->set('vv_primary_link_obj', $plObj); + + // While we're here, note the CO since we'll probably need it soon + if(!empty($plObj->co_id)) { + $this->cur_pl->co_id = $plObj->co_id; + } elseif(method_exists($linkTable, 'findCoForRecord')) { + $this->cur_pl->co_id = $linkTable->findCoForRecord((int)$this->cur_pl->value); } } - + catch(RecordNotFoundException $e) { + $this->llog('error', "Could not find value '" . $this->cur_pl->value . "' for primary link object " . $linkTableName); + // Mask this with a generic UnauthorizedException + throw new UnauthorizedException(__d('error', 'perm')); + } + return $this->cur_pl; } - + /** * Get the redirect goal for this table. * + * @param string $action Action + * + * @return string|null Redirect goal * @since COmanage Registry v5.0.0 - * @param string $action Action - * @return string Redirect goal */ protected function getRedirectGoal(string $action): ?string { From c71d9c4d1951933a1b76d712b2c34cd5a3c3d970 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 23 Jul 2024 14:29:36 +0300 Subject: [PATCH 4/8] Transmogrification sql typo.External identity Source link fix --- app/src/Command/TransmogrifyCommand.php | 4 ++-- app/templates/ExternalIdentities/fields-nav.inc | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/Command/TransmogrifyCommand.php b/app/src/Command/TransmogrifyCommand.php index c0cc13df2..a332ce577 100644 --- a/app/src/Command/TransmogrifyCommand.php +++ b/app/src/Command/TransmogrifyCommand.php @@ -1308,8 +1308,8 @@ protected function roleSqlSelect(string $tableName): string { manager_co_person_id, cou_id, CASE - WHEN affiliation IS NULL THEN "member" - WHEN affiliation = "" THEN "member" + WHEN affiliation IS NULL THEN 'member' + WHEN affiliation = '' THEN 'member' ELSE affiliation END as affiliation, title, diff --git a/app/templates/ExternalIdentities/fields-nav.inc b/app/templates/ExternalIdentities/fields-nav.inc index 1ac066f34..fc1fa431e 100644 --- a/app/templates/ExternalIdentities/fields-nav.inc +++ b/app/templates/ExternalIdentities/fields-nav.inc @@ -55,7 +55,10 @@ $topLinks = [ ] ] ], - [ +]; + +if($vv_obj?->ext_identity_source_record?->id !== null) { + $topLinks[] = [ 'icon' => 'visibility', 'order' => 'Default', 'class' => 'cm-modal-link nospin', // launch this in a modal @@ -68,8 +71,8 @@ $topLinks = [ 'action' => 'view', $vv_obj->ext_identity_source_record->id ] - ] -]; + ]; +} // $addMenuLinks is also given slightly different treatment from the typical $topLinks found in most views: // it is a page-global menu used for adding MVEAs and is given special treatment in element/mveaCanvas.php. From bc13667ad56f6c98c016fe362b1e24c02cc70633 Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 23 Jul 2024 16:45:16 +0300 Subject: [PATCH 5/8] Fix Configurtion Breadcrumb for isConfigurationTables --- app/src/Controller/Component/BreadcrumbComponent.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index 16a3a99d7..071733ee8 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -111,8 +111,9 @@ public function beforeRender(EventInterface $event) { // Do we have a target model, and if so is it a configuration // model (eg: ApiUsers) or an object model (eg: CoPeople)? - if(isset($controller->$modelsName) // May not be set under certain error conditions - && method_exists($controller->$modelsName, "isConfigurationTable")) { + if(\is_object($controller->$modelsName) + && method_exists($controller->$modelsName, "isConfigurationTable") + ) { $controller->set('vv_bc_configuration_link', $controller->$modelsName->isConfigurationTable()); } else { $controller->set('vv_bc_configuration_link', false); From aca14f75b4d4a5965030bdb70a2081349eb0cd8d Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Tue, 23 Jul 2024 18:41:18 +0300 Subject: [PATCH 6/8] Fix ExternalIdentitySource alert link to open in modal. Fixed modal links to open on target _top --- app/resources/locales/en_US/information.po | 7 +++-- .../ExternalIdentitySources/retrieve.php | 26 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/resources/locales/en_US/information.po b/app/resources/locales/en_US/information.po index 85a1b293f..b661506a9 100644 --- a/app/resources/locales/en_US/information.po +++ b/app/resources/locales/en_US/information.po @@ -61,7 +61,10 @@ msgid "ExternalIdentitySources.records" msgstr "Source Records" msgid "ExternalIdentitySources.retrieve" -msgstr "This is the current record retrieved directly from the source. View the latest record cached by Registry." +msgstr "This is the current record retrieved directly from the source. {0}" + +msgid "ExternalIdentitySources.cached" +msgstr "View the latest record cached by Registry" msgid "ExternalIdentitySources.retrieve.notSynced" msgstr "This is the current record available directly from the source." @@ -70,7 +73,7 @@ msgid "ExternalIdentitySourceRecords.metadata" msgstr "Metadata" msgid "ExternalIdentitySourceRecords.view" -msgstr "This is the latest record from the source as cached by Registry. Retrieve the current record directly from the source." +msgstr "This is the latest record from the source as cached by Registry. Retrieve the current record directly from the source." msgid "ExternalIdentitySources.search.attrs.none" msgstr "The External Identity Source cannot be searched." diff --git a/app/templates/ExternalIdentitySources/retrieve.php b/app/templates/ExternalIdentitySources/retrieve.php index b9b61a094..b7c0cca94 100644 --- a/app/templates/ExternalIdentitySources/retrieve.php +++ b/app/templates/ExternalIdentitySources/retrieve.php @@ -94,18 +94,26 @@ id)) { + // Construct the link + $link = $this->Html->link( + __d('information', 'ExternalIdentitySources.cached'), + [ + 'controller' => 'ext-identity-source-records', + 'action' => 'view', + $vv_external_identity_record->id + ], + [ + 'class' => 'cm-modal-link nospin', + 'target' => '_top', + 'data-cm-modal-title' => __d('controller', 'ExtIdentitySourceRecords', 1) + ], + ); + + // Construct the message $noticeText = __d( 'information', 'ExternalIdentitySources.retrieve', - [ - $this->Url->build( - [ - 'controller' => 'ext-identity-source-records', - 'action' => 'view', - $vv_external_identity_record->id - ] - ) - ] + $link ); } ?> From 23a1822ea27b79486a0bfcb5a3f8eccc2cee029d Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 4 Sep 2024 18:22:17 +0300 Subject: [PATCH 7/8] fix typo --- app/src/Controller/AppController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 51ff92c50..efb407076 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -333,8 +333,9 @@ protected function populatedPrimaryLink(string $potentialPrimaryLink): Object * Perform primary link lookup. * * @throws \RuntimeException Exception thrown if a primary link is empty and it is not allowed - * @since Symfony v7.0.3 - */ + * @since COmanage Registry v5.0.0 + */ + protected function primaryLinkLookup(): void { // $this->name = Models From a1e40ad559c2c4ae2f8f5bb0bc40a7eabb75ac4d Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Wed, 4 Sep 2024 19:33:14 +0300 Subject: [PATCH 8/8] Change inherited class from MVEAController to StandardController --- app/src/Controller/ExtIdentitySourceRecordsController.php | 2 +- app/src/Controller/StandardController.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/Controller/ExtIdentitySourceRecordsController.php b/app/src/Controller/ExtIdentitySourceRecordsController.php index 7f24d4fd1..d7e463c20 100644 --- a/app/src/Controller/ExtIdentitySourceRecordsController.php +++ b/app/src/Controller/ExtIdentitySourceRecordsController.php @@ -33,7 +33,7 @@ use Cake\Log\Log; use Cake\ORM\TableRegistry; -class ExtIdentitySourceRecordsController extends MVEAController { +class ExtIdentitySourceRecordsController extends StandardController { public $paginate = [ 'order' => [ 'ExtIdentitySourceRecords.id' => 'asc' diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index 3b9c68abe..c6f2c3ef1 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -207,12 +207,12 @@ public function beforeRender(\Cake\Event\EventInterface $event) { // Primarily of interest to detailed record views, if this attribute supports // Pipeline sourcing (ie: has a source_foo_id field) set the name of the source // foreign key into a view var since it's not always calculable. - if(method_exists($table, "sourceForeignKey")) { + if(method_exists($table, 'sourceForeignKey')) { $this->set('vv_source_fk', $table->sourceForeignKey()); } // Check to see if the model names a specific layout - if(method_exists($table, "getLayout")) { + if(method_exists($table, 'getLayout')) { $this->viewBuilder()->setLayout($table->getLayout($this->request->getParam('action'))); }