TenantAtlas/apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php
ahmido eced9ad50c Spec 315: implement environment CTA explicit filter contract (#370)
## Summary
- hard-cut environment-owned CTA links into workspace hubs to canonical `environment_id` filters
- add shared workspace-hub environment filter resolution and visible filtered-state rendering across in-scope hubs
- update workspace hub pages, link helpers, and focused test coverage for explicit environment CTA filtering

## Validation
- Not run in this workflow

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #370
2026-05-16 11:50:20 +00:00

351 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Navigation;
use Illuminate\Http\Request;
final class WorkspaceHubRegistry
{
/**
* @var list<string>
*/
private const FORBIDDEN_QUERY_KEYS = [
'tenant',
'tenant_id',
'managed_environment_id',
'environment_id',
'environment',
'tenant_scope',
'tableFilters',
];
/**
* @var list<string>
*/
private const ENVIRONMENT_FILTER_KEYS = [
'tenant',
'tenant_id',
'managed_environment_id',
'environment_id',
'environment',
'tenant_scope',
];
/**
* @var list<string>
*/
private const ENVIRONMENT_FILTER_QUERY_KEYS = [
'environment_id',
];
/**
* @var array<string, array{label: string, path: string, pattern: string}>
*/
private const HUBS = [
'workspace_home' => [
'label' => 'Workspace Overview',
'path' => '/admin',
'pattern' => '#^/admin/?$#',
],
'workspace_overview' => [
'label' => 'Workspace Overview',
'path' => '/admin/workspaces/{workspace}/overview',
'pattern' => '#^/admin/workspaces/[^/]+/overview/?$#',
],
'operations' => [
'label' => 'Operations',
'path' => '/admin/workspaces/{workspace}/operations',
'pattern' => '#^/admin/workspaces/[^/]+/operations/?$#',
],
'provider_connections' => [
'label' => 'Provider Connections',
'path' => '/admin/provider-connections',
'pattern' => '#^/admin/provider-connections(?:/.*)?$#',
],
'finding_exceptions_queue' => [
'label' => 'Finding Exceptions Queue',
'path' => '/admin/finding-exceptions/queue',
'pattern' => '#^/admin/finding-exceptions/queue/?$#',
],
'evidence_overview' => [
'label' => 'Evidence Overview',
'path' => '/admin/evidence/overview',
'pattern' => '#^/admin/evidence/overview/?$#',
],
'review_register' => [
'label' => 'Review Register',
'path' => '/admin/reviews',
'pattern' => '#^/admin/reviews/?$#',
],
'customer_review_workspace' => [
'label' => 'Customer Review Workspace',
'path' => '/admin/reviews/workspace',
'pattern' => '#^/admin/reviews/workspace/?$#',
],
'governance_inbox' => [
'label' => 'Governance Inbox',
'path' => '/admin/governance/inbox',
'pattern' => '#^/admin/governance/inbox/?$#',
],
'decision_register' => [
'label' => 'Decision Register',
'path' => '/admin/governance/decisions',
'pattern' => '#^/admin/governance/decisions/?$#',
],
'audit_log' => [
'label' => 'Audit Log',
'path' => '/admin/audit-log',
'pattern' => '#^/admin/audit-log/?$#',
],
'alerts' => [
'label' => 'Alerts',
'path' => '/admin/alerts',
'pattern' => '#^/admin/alerts/?$#',
],
'alert_deliveries' => [
'label' => 'Alert Deliveries',
'path' => '/admin/alerts/alert-deliveries',
'pattern' => '#^/admin/alerts/alert-deliveries(?:/.*)?$#',
],
'alert_rules' => [
'label' => 'Alert Rules',
'path' => '/admin/alerts/alert-rules',
'pattern' => '#^/admin/alerts/alert-rules(?:/.*)?$#',
],
'alert_destinations' => [
'label' => 'Alert Destinations',
'path' => '/admin/alerts/alert-destinations',
'pattern' => '#^/admin/alerts/alert-destinations(?:/.*)?$#',
],
'workspace_settings' => [
'label' => 'Workspace Settings',
'path' => '/admin/settings/workspace',
'pattern' => '#^/admin/settings/workspace/?$#',
],
'manage_workspaces' => [
'label' => 'Manage Workspaces',
'path' => '/admin/workspaces',
'pattern' => '#^/admin/workspaces/?$#',
],
'managed_environments_landing' => [
'label' => 'Managed Environments Landing',
'path' => '/admin/workspaces/{workspace}/environments',
'pattern' => '#^/admin/workspaces/[^/]+/environments/?$#',
],
];
/**
* @var array<string, array{label: string, path: string, pattern: string}>
*/
private const EXCLUSIONS = [
'environment_dashboard' => [
'label' => 'Environment Dashboard',
'path' => '/admin/workspaces/{workspace}/environments/{environment}',
'pattern' => '#^/admin/workspaces/[^/]+/environments/[^/]+(?:/.*)?$#',
],
'stored_reports_environment_routes' => [
'label' => 'Stored Reports environment routes',
'path' => '/admin/workspaces/{workspace}/environments/{environment}/stored-reports',
'pattern' => '#^/admin/workspaces/[^/]+/environments/[^/]+/stored-reports(?:/.*)?$#',
],
'support_request_action_surface' => [
'label' => 'Support Request action surface',
'path' => 'modal/action-only',
'pattern' => '#^$#',
],
];
/**
* @return array<string, array{label: string, path: string, pattern: string}>
*/
public static function entries(): array
{
return self::HUBS;
}
/**
* @return array<string, array{label: string, path: string, pattern: string}>
*/
public static function exclusions(): array
{
return self::EXCLUSIONS;
}
/**
* @return list<string>
*/
public static function forbiddenQueryKeys(): array
{
return self::FORBIDDEN_QUERY_KEYS;
}
/**
* @return list<string>
*/
public static function environmentLikeFilterKeys(): array
{
return self::ENVIRONMENT_FILTER_KEYS;
}
public static function isWorkspaceHubPath(string $path): bool
{
$normalizedPath = self::normalizePath($path);
foreach (self::HUBS as $hub) {
if (preg_match($hub['pattern'], $normalizedPath) === 1) {
return true;
}
}
return false;
}
public static function isExplicitlyExcludedPath(string $path): bool
{
$normalizedPath = self::normalizePath($path);
foreach (self::EXCLUSIONS as $exclusion) {
if (preg_match($exclusion['pattern'], $normalizedPath) === 1) {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $query
* @return array<string, mixed>
*/
public static function cleanQuery(array $query): array
{
foreach (self::FORBIDDEN_QUERY_KEYS as $key) {
unset($query[$key]);
}
return $query;
}
/**
* @param array<string, mixed> $parameters
* @return array<string, mixed>
*/
public static function cleanParameters(array $parameters): array
{
return self::cleanQuery($parameters);
}
public static function cleanUrl(string $url): string
{
if (! str_contains($url, '?')) {
return $url;
}
$parts = parse_url($url);
if (! is_array($parts)) {
return $url;
}
$query = [];
parse_str((string) ($parts['query'] ?? ''), $query);
$parts['query'] = http_build_query(self::cleanQuery($query), '', '&', PHP_QUERY_RFC3986);
return self::buildUrl($parts);
}
public static function hasForbiddenQuery(string $url): bool
{
$parts = parse_url($url);
if (! is_array($parts) || ! isset($parts['query'])) {
return false;
}
$query = [];
parse_str((string) $parts['query'], $query);
foreach (self::FORBIDDEN_QUERY_KEYS as $key) {
if (array_key_exists($key, $query)) {
return true;
}
}
return false;
}
public static function isCleanWorkspaceHubEntry(?Request $request = null): bool
{
$request ??= request();
return self::isWorkspaceHubPath('/'.ltrim((string) $request->path(), '/'))
&& ! self::requestHasEnvironmentFilterQuery($request);
}
public static function requestHasEnvironmentFilterQuery(?Request $request = null): bool
{
$request ??= request();
foreach (self::ENVIRONMENT_FILTER_QUERY_KEYS as $key) {
if ($request->query->has($key) && filled($request->query($key))) {
return true;
}
}
return false;
}
private static function normalizePath(string $path): string
{
$parsedPath = parse_url($path, PHP_URL_PATH);
$path = is_string($parsedPath) && $parsedPath !== '' ? $parsedPath : $path;
return '/'.trim($path, '/');
}
/**
* @param array<string, mixed> $parts
*/
private static function buildUrl(array $parts): string
{
$url = '';
if (isset($parts['scheme'])) {
$url .= $parts['scheme'].'://';
}
if (isset($parts['user'])) {
$url .= $parts['user'];
if (isset($parts['pass'])) {
$url .= ':'.$parts['pass'];
}
$url .= '@';
}
if (isset($parts['host'])) {
$url .= $parts['host'];
}
if (isset($parts['port'])) {
$url .= ':'.$parts['port'];
}
$url .= $parts['path'] ?? '';
if (($parts['query'] ?? '') !== '') {
$url .= '?'.$parts['query'];
}
if (isset($parts['fragment'])) {
$url .= '#'.$parts['fragment'];
}
return $url;
}
}