## 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
351 lines
9.7 KiB
PHP
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;
|
|
}
|
|
}
|