diff --git a/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po b/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po index fdf0f1eb3..f1f9f8dc0 100644 --- a/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po +++ b/app/availableplugins/SqlConnector/resources/locales/en_US/sql_connector.po @@ -25,6 +25,9 @@ msgid "controller.SqlProvisioners" msgstr "{0,plural,=1{SQL Provisioner} other{SQL Provisioners}}" +msgid "display.SqlSource" +msgstr "{0} Source" + msgid "enumeration.SqlSourceTableModeEnum.FL" msgstr "Flat" diff --git a/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php b/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php index a0eb81ff4..9d71f7d51 100644 --- a/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php +++ b/app/availableplugins/SqlConnector/src/Model/Table/SqlSourcesTable.php @@ -45,6 +45,7 @@ class SqlSourcesTable extends Table { use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -106,6 +107,16 @@ public function initialize(array $config): void { $this->setPrimaryLink(['external_identity_source_id']); $this->setRequiresCO(true); + + $this->setEditContains([ + 'Servers' => ['SqlServers'], + 'ExternalIdentitySources', + ]); + + $this->setViewContains([ + 'Servers' => ['SqlServers'], + 'ExternalIdentitySources', + ]); $this->setAutoViewVars([ 'addressTypes' => [ @@ -230,6 +241,18 @@ protected function getChanges( return false; } + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.2.0 + * @param \SqlConnector\Model\Entity\SqlSource $entity Entity to generate display field for + * @return string Display field + */ + public function generateDisplayField(\SqlConnector\Model\Entity\SqlSource $entity): string { + return __d('sql_connector', 'display.SqlSource', [$entity->external_identity_source->description]); + } + + /** * Obtain the set of changed records from the source database. * diff --git a/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php b/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php index 09530b3de..3060bb4af 100644 --- a/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php +++ b/app/plugins/CoreServer/src/Controller/MatchServerAttributesController.php @@ -30,12 +30,44 @@ namespace CoreServer\Controller; use App\Controller\StandardPluginController; +use App\Lib\Util\StringUtilities; use Cake\Event\EventInterface; class MatchServerAttributesController extends StandardPluginController { + use \App\Lib\Traits\BreadcrumbsTrait; + protected array $paginate = [ 'order' => [ 'MatchServerAttributes.attribute' => 'asc' ] ]; + + /** + * Callback run prior to the request render. + * + * @since COmanage Registry v5.2.0 + * @param EventInterface $event Cake Event + * @return \Cake\Http\Response HTTP Response + */ + + public function beforeRender(\Cake\Event\EventInterface $event) + { + // Build standard server breadcrumbs from *_server_id + $customParents = $this->buildServerParamBreadcrumbs(); + + if (!empty($customParents)) { + $vv_bc_parents = (array)$this->viewBuilder()->getVar('vv_bc_parents'); + $vv_bc_parents = [...$customParents, ...$vv_bc_parents]; + $this->set('vv_bc_parents', $vv_bc_parents); + } + + $title = __d('core_server', 'controller.MatchServerAttributes', [99]); + if(in_array($this->request->getParam('action'), ['add', 'edit'])) { + $title = __d('operation', strtolower($this->request->getParam('action')) . '.a', [$title]); + } + + $this->set('vv_title', $title); + + return parent::beforeRender($event); + } } diff --git a/app/plugins/EnvSource/resources/locales/en_US/env_source.po b/app/plugins/EnvSource/resources/locales/en_US/env_source.po index 2cf309708..bf902af83 100644 --- a/app/plugins/EnvSource/resources/locales/en_US/env_source.po +++ b/app/plugins/EnvSource/resources/locales/en_US/env_source.po @@ -31,6 +31,9 @@ msgstr "{0,plural,=1{Env Source} other{Env Sources}}" msgid "controller.PetitionEnvIdentities" msgstr "{0,plural,=1{Petition Env Identity} other{Petition Env Identities}}" +msgid "display.EnvSource" +msgstr "{0} Source" + msgid "enumeration.EnvSourceSpModeEnum.O" msgstr "Other" diff --git a/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php b/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php index 48e62f736..398a231eb 100644 --- a/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php +++ b/app/plugins/EnvSource/src/Model/Table/EnvSourcesTable.php @@ -40,6 +40,7 @@ class EnvSourcesTable extends Table { use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TabTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -97,6 +98,14 @@ public function initialize(array $config): void { $this->setPrimaryLink(['external_identity_source_id']); $this->setRequiresCO(true); + $this->setEditContains([ + 'ExternalIdentitySources', + ]); + + $this->setViewContains([ + 'ExternalIdentitySources', + ]); + // All the tabs share the same configuration in the ModelTable file $this->setTabsConfig( [ @@ -156,6 +165,17 @@ public function initialize(array $config): void { ]); } + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.2.0 + * @param \EnvSource\Model\Entity\EnvSource $entity Entity to generate display field for + * @return string Display field + */ + public function generateDisplayField(\EnvSource\Model\Entity\EnvSource $entity): string { + return __d('env_source', 'display.EnvSource', [$entity->external_identity_source->description]); + } + /** * Obtain the set of changed records from the source database. * diff --git a/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php b/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php index d02e776f9..9297ddd1b 100644 --- a/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php +++ b/app/plugins/SshKeyAuthenticator/src/Controller/SshKeysController.php @@ -29,6 +29,7 @@ namespace SshKeyAuthenticator\Controller; +use Cake\Event\EventInterface; use Cake\ORM\TableRegistry; use App\Controller\MultipleAuthenticatorController; use App\Lib\Util\StringUtilities; diff --git a/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php index 7d2c8ff70..1a6978365 100644 --- a/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php +++ b/app/plugins/SshKeyAuthenticator/src/Model/Table/SshKeyAuthenticatorsTable.php @@ -39,6 +39,7 @@ class SshKeyAuthenticatorsTable extends Table { use \App\Lib\Traits\LabeledLogTrait; use \App\Lib\Traits\PermissionsTrait; use \App\Lib\Traits\PrimaryLinkTrait; + use \App\Lib\Traits\QueryModificationTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -74,6 +75,14 @@ public function initialize(array $config): void { $this->setPrimaryLink('authenticator_id'); $this->setRequiresCO(true); + $this->setEditContains([ + 'Authenticators', + ]); + + $this->setViewContains([ + 'Authenticators', + ]); + $this->setPermissions([ // Actions that operate over an entity (ie: require an $id) 'entity' => [ @@ -89,6 +98,17 @@ public function initialize(array $config): void { ]); } + /** + * Table specific logic to generate a display field. + * + * @since COmanage Registry v5.2.0 + * @param \SshKeyAuthenticator\Model\Entity\SshKeyAuthenticator $entity Entity to generate display field for + * @return string Display field + */ + public function generateDisplayField(\SshKeyAuthenticator\Model\Entity\SshKeyAuthenticator $entity): string { + return $entity->authenticator->description; + } + /** * Set validation rules. * diff --git a/app/src/Controller/Component/BreadcrumbComponent.php b/app/src/Controller/Component/BreadcrumbComponent.php index 903c47898..ccd86e65a 100644 --- a/app/src/Controller/Component/BreadcrumbComponent.php +++ b/app/src/Controller/Component/BreadcrumbComponent.php @@ -227,7 +227,8 @@ public function injectPrimaryLink(object $link, bool $index = true, string $link ); return $canEdit ? 'edit' : ($canView ? 'view' : ''); - } + }, + forChainItem: true ); $linkTable = \Cake\ORM\TableRegistry::getTableLocator()->get($linkModelFqn); @@ -249,11 +250,10 @@ public function injectPrimaryLink(object $link, bool $index = true, string $link if ($index && method_exists($linkTable, 'findPrimaryLink')) { $parentLink = $linkTable->findPrimaryLink($linkedEntity->id); - // https://comanage-ioi-dev.workbench.incommon.org/registry-pe/authenticators?co_id=2 $this->injectParents[strtolower($linkModelFqn) . ':index'] = [ 'target' => [ - 'plugin' => $parentLink->plugin ?? null, - 'controller' => $linkModelFqn, + 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, + 'controller' => StringUtilities::pluginModel($linkModelFqn), 'action' => 'index', '?' => [ $parentLink->attr => $parentLink->value @@ -283,9 +283,9 @@ public function injectPrimaryLink(object $link, bool $index = true, string $link // Inject the entity breadcrumb (unique per table:id) $this->injectParents[$this->composeEntityKey($linkTable->getTable(), (int)$linkedEntity->id)] = [ 'target' => [ - 'plugin' => $link->plugin ?? null, - 'controller' => $linkModelFqn, - 'action' => $breadcrumbAction, + 'plugin' => $parentLink->plugin ?? StringUtilities::blankToNull(StringUtilities::pluginPlugin($linkModelFqn)) ?? null, + 'controller' => StringUtilities::pluginModel($linkModelFqn), + 'action' => $breadcrumbAction, (int)$linkedEntity->id ], 'label' => $linkLabel ?? $title, @@ -391,6 +391,7 @@ private function resolveContainList($table, string $mappedAction): array * @param int|null $currentId Current entity ID * @param array $pagePermissions Permissions for the current page * @param callable $peopleActionOverride Override callback for People actions + * @param bool $forChainItem Explicit call for a chain item (e.g. add) * @return string Mapped action name * @since COmanage Registry v5.2.0 */ @@ -398,23 +399,30 @@ private function mapActionForBreadcrumb( string $requestAction, ?int $currentId, array $pagePermissions, - callable $peopleActionOverride + callable $peopleActionOverride, + bool $forChainItem ): string { - // Custom override for People if provided $override = $peopleActionOverride(); if ($override !== '') { return $override; } - if (in_array($requestAction, ['index', 'view', 'delete', 'add', 'edit'], true)) { - return $requestAction; - } - - if ($currentId !== null) { - return (!empty($pagePermissions['edit'])) ? 'edit' : 'view'; + if ($forChainItem) { + // Never render 'add' for chain items + if ($requestAction === 'add') { + return 'index'; + } + // If the current page is edit/view/delete (and thus has an entity), prefer edit/view + if (in_array($requestAction, ['edit', 'view', 'delete'], true) && $currentId !== null) { + return (!empty($pagePermissions['edit'])) ? 'edit' : 'view'; + } + return in_array($requestAction, ['index', 'view', 'delete', 'edit'], true) + ? $requestAction + : 'index'; } - return 'index'; + // If you ever reuse this for non-chain items, decide appropriate behavior here + return $requestAction; } /** @@ -427,15 +435,17 @@ private function mapActionForBreadcrumb( */ private function determineEntityAction($entity, string $mappedAction): string { - if ($mappedAction === 'add' || $mappedAction === 'delete') { - return $mappedAction; + // Only allow 'delete' to pass through; never return 'add' for existing entities + if ($mappedAction === 'delete') { + return 'delete'; } if (method_exists($entity, 'isReadOnly')) { return $entity->isReadOnly() ? 'view' : 'edit'; } - return $mappedAction; + // Fall back to mapped action when not 'add'/'delete' + return $mappedAction === 'add' ? 'view' : $mappedAction; } /** diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php index 31c936b7f..0bab00bc6 100644 --- a/app/src/Controller/StandardPluginController.php +++ b/app/src/Controller/StandardPluginController.php @@ -67,9 +67,14 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { $this->Breadcrumb->skipConfig(['/^\//']); } - // The authenticator routes have a unique patern. We will not inject the Breadcrumb here but + // The authenticator routes have a unique pattern. We will not inject the Breadcrumb here, but // we will construct it in the MultipleAuthtenticatorController. - if(!str_ends_with($primaryLink->attr, '_authenticator_id')) { + // For very deep breadcrumbs we need to skip the generic rule + // and allow the controller to handle it. + if( + !str_ends_with($primaryLink->attr, '_authenticator_id') + && !str_ends_with($primaryLink->attr, '_server_id') + ) { $this->Breadcrumb->injectPrimaryLink($primaryLink); } } diff --git a/app/src/Lib/Traits/BreadcrumbsTrait.php b/app/src/Lib/Traits/BreadcrumbsTrait.php index f4bae6d6e..2432812da 100644 --- a/app/src/Lib/Traits/BreadcrumbsTrait.php +++ b/app/src/Lib/Traits/BreadcrumbsTrait.php @@ -29,7 +29,105 @@ namespace App\Lib\Traits; +use App\Lib\Util\StringUtilities; + trait BreadcrumbsTrait { + /** + * Builds breadcrumbs from a `*_server_id` query parameter. + * + * Supported keys include (but are not limited to): + * - match_server_id → CoreServer.MatchServers + * - http_server_id → CoreServer.HttpServers + * - sql_server_id → CoreServer.SqlServers + * - smtp_server_id → CoreServer.SmtpServers + * - oauth2_server_id → CoreServer.Oauth2Servers + * + * @return array Breadcrumb parents to prepend + */ + protected function buildServerParamBreadcrumbs(): array + { + $vv_bc_parents = (array)$this->viewBuilder()->getVar('vv_bc_parents'); + + // 1) Find the first *_server_id query param and capture its key & value + $q = (array)$this->getRequest()->getQueryParams(); + $serverFkKey = null; // eg: match_server_id + $serverFkVal = 0; // eg: 42 + + foreach ($q as $k => $v) { + if (is_string($k) && preg_match('/^[a-z0-9_]+_server_id$/', $k)) { + $serverFkKey = $k; + $serverFkVal = (int)$v; + break; + } + } + + if (!$serverFkKey || $serverFkVal <= 0) { + // No *_server_id param present + return []; + } + + // 2) Infer the child table class from the foreign key, eg: match_server_id → MatchServers + // We’ll attempt CoreServer.PluginTable first, then fall back to the bare alias. + $modelsName = StringUtilities::foreignKeyToClassName($serverFkKey); // eg: MatchServers + // Get the child table class + $ChildTable = $this->getTableLocator()->get('CoreServer.' . $modelsName); + + // 3) Fetch the child entity (eg: MatchServers/SqlServers/HttpServers/etc) to obtain server_id + $child = null; + $serverId = 0; + try { + $child = $ChildTable->get($serverFkVal); + $serverId = (int)($child->server_id ?? 0); + } catch (\Throwable $e) { + // Fail-soft: no child → no crumbs + return []; + } + + $parents = []; + + // 4) Servers index (for this CO) + $coId = method_exists($this, 'getCOID') ? (int)$this->getCOID() : 0; + $serversIndexKey = 'servers:' . $coId; + if ($coId > 0 && empty($vv_bc_parents[$serversIndexKey])) { + $parents[$serversIndexKey] = [ + 'label' => StringUtilities::localizeController('Servers', null, true), + 'target' => [ + 'plugin' => null, + 'controller' => 'Servers', + 'action' => 'index', + '?' => ['co_id' => $coId], + ], + ]; + } + + // 5) Current Server → Servers/edit/{server_id} + if ($serverId > 0) { + try { + $Servers = $this->fetchTable('Servers'); + $server = $Servers->get($serverId); + $serverKey = 'servers:' . $serverId; + + if (empty($vv_bc_parents[$serverKey])) { + $parents[$serverKey] = [ + 'label' => method_exists($Servers, 'generateDisplayField') + ? $Servers->generateDisplayField($server) + : ($server->{$Servers->getDisplayField()} ?? ('Server #' . $serverId)), + 'target' => [ + 'plugin' => null, + 'controller' => 'Servers', + 'action' => 'edit', + $serverId, + ], + ]; + } + } catch (\Throwable $e) { + // Skip this crumb if we can’t load the parent server + } + } + + return $parents; + } + /** * Build the common person-centric breadcrumb parents. * diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index e3a6328fc..feed2b562 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -58,6 +58,23 @@ public static function addDashesToToken(string $token, int $jump = 4): string { return $dtoken; } + + /** + * Convert empty or whitespace-only strings to null. + * Trims the input string and returns null if empty after trimming. + * + * @param string|null $s String to process + * @return string|null Trimmed string or null if empty + * @since COmanage Registry v5.2.0 + */ + public static function blankToNull(?string $s): ?string { + if ($s === null) { + return null; + } + $t = trim($s); + return $t === '' ? null : $t; + } + /** * Determine the foreign key name to point to a Cake Class Name (eg: foo_id for Foo). * @@ -382,16 +399,18 @@ public static function foreignKeyToController(string $s): string { * @return string Localized text string * @since COmanage Registry v5.0.0 */ - - public static function localizeController(string $controllerName, ?string $pluginName, bool $plural=false): string { - if($pluginName) { - // Localize via plugin - return __d(Inflector::underscore($pluginName), 'controller.'.$controllerName, [$plural ? 99 : 1]); - } else { - // Standard localization + public static function localizeController(string $controllerName, ?string $pluginName, bool $plural = false): string { + // If "Plugin.Model" was passed, reduce to just "Model" + if (str_contains($controllerName, '.')) { + $controllerName = self::pluginModel($controllerName); // returns the part after the dot + } - return __d('controller', $controllerName, [$plural ? 99 : 1]); + if ($pluginName) { + // Localize via plugin + return __d(\Cake\Utility\Inflector::underscore($pluginName), 'controller.' . $controllerName, [$plural ? 99 : 1]); } + // Standard Localization + return __d('controller', $controllerName, [$plural ? 99 : 1]); } /** @@ -417,11 +436,13 @@ public static function qualifyModelPath(string $modelPath, ?string $plugin): str * @param string $s Plugin path, in Plugin.Model format. * @return string Model name */ - - public static function pluginModel(string $s): string { - $bits = explode('.', $s, 2); - return $bits[1]; + public static function pluginModel(string $s): string { + if (str_contains($s, '.')) { + [, $model] = explode('.', $s, 2); + return $model; + } + return $s; } /** @@ -431,11 +452,17 @@ public static function pluginModel(string $s): string { * @param string $s Plugin path, in Plugin.Model format. * @return string Plugin name */ - - public static function pluginPlugin(string $s): string { - $bits = explode('.', $s, 2); - return $bits[0]; + /** + * Determine the plugin component of a Plugin path. + * Returns "" (empty string) if no plugin is present. + */ + public static function pluginPlugin(string $s): string { + if (str_contains($s, '.')) { + [$plugin] = explode('.', $s, 2); + return $plugin; + } + return ''; } /** diff --git a/app/src/Model/Table/ServersTable.php b/app/src/Model/Table/ServersTable.php index a626f4681..678b20bb1 100644 --- a/app/src/Model/Table/ServersTable.php +++ b/app/src/Model/Table/ServersTable.php @@ -58,7 +58,9 @@ public function initialize(array $config): void { $this->addBehavior('Changelog'); $this->addBehavior('Log'); $this->addBehavior('Timestamp'); - + + // XXX This is set to be a primary table. Nevertheless we access it via the configuration view + // and when clicked we do not get the configuration breadcrumb.??? $this->setTableType(\App\Lib\Enum\TableTypeEnum::Primary); // Define associations