Implemented the first version of review output resolve actions. Included a ReviewOutputResolveActionMapper, commands to seed browser fixtures, updated CustomerReviewWorkspace, EnvironmentReviewResource, UI enforcement, and related views. Also added extensive unit, feature, and browser tests, and updated the design coverage matrix. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #422
216 lines
7.2 KiB
PHP
216 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ResolutionGuidance;
|
|
|
|
final class ResolutionAction
|
|
{
|
|
public const string TYPE_NAVIGATION = 'navigation';
|
|
|
|
public const string TYPE_DOWNLOAD = 'download';
|
|
|
|
public const string TYPE_DISCLOSURE = 'disclosure';
|
|
|
|
public const string TYPE_NONE = 'none';
|
|
|
|
public const string TYPE_DOMAIN_ACTION = 'domain_action';
|
|
|
|
public const string TYPE_OPERATION_ACTION = 'operation_action';
|
|
|
|
/**
|
|
* @param array{
|
|
* key?:mixed,
|
|
* label?:mixed,
|
|
* type?:mixed,
|
|
* url?:mixed,
|
|
* icon?:mixed,
|
|
* kind?:mixed,
|
|
* action_name?:mixed,
|
|
* capability?:mixed,
|
|
* requires_confirmation?:mixed,
|
|
* audit_event?:mixed,
|
|
* operation_run_type?:mixed,
|
|
* disabled_reason?:mixed
|
|
* }|null $action
|
|
* @return array{
|
|
* key:string,
|
|
* label:string,
|
|
* type:string,
|
|
* url:?string,
|
|
* icon:string,
|
|
* kind:string,
|
|
* action_name:?string,
|
|
* capability:?string,
|
|
* requires_confirmation:bool,
|
|
* audit_event:?string,
|
|
* operation_run_type:?string,
|
|
* disabled_reason:?string
|
|
* }
|
|
*/
|
|
public static function fromArray(?array $action, string $fallbackKey, string $fallbackLabel = 'Unavailable'): array
|
|
{
|
|
$label = is_string($action['label'] ?? null) && trim((string) $action['label']) !== ''
|
|
? trim((string) $action['label'])
|
|
: $fallbackLabel;
|
|
$url = is_string($action['url'] ?? null) && trim((string) $action['url']) !== ''
|
|
? trim((string) $action['url'])
|
|
: null;
|
|
$rawType = is_string($action['type'] ?? null) && trim((string) $action['type']) !== ''
|
|
? trim((string) $action['type'])
|
|
: null;
|
|
$rawKind = is_string($action['kind'] ?? null) && trim((string) $action['kind']) !== ''
|
|
? trim((string) $action['kind'])
|
|
: null;
|
|
$key = is_string($action['key'] ?? null) && trim((string) $action['key']) !== ''
|
|
? trim((string) $action['key'])
|
|
: $fallbackKey;
|
|
$type = $rawType ?? self::typeFromKind($rawKind, $url);
|
|
$actionName = is_string($action['action_name'] ?? null) && trim((string) $action['action_name']) !== ''
|
|
? trim((string) $action['action_name'])
|
|
: null;
|
|
$capability = is_string($action['capability'] ?? null) && trim((string) $action['capability']) !== ''
|
|
? trim((string) $action['capability'])
|
|
: null;
|
|
$requiresConfirmation = (bool) ($action['requires_confirmation'] ?? false);
|
|
$auditEvent = is_string($action['audit_event'] ?? null) && trim((string) $action['audit_event']) !== ''
|
|
? trim((string) $action['audit_event'])
|
|
: null;
|
|
$operationRunType = is_string($action['operation_run_type'] ?? null) && trim((string) $action['operation_run_type']) !== ''
|
|
? trim((string) $action['operation_run_type'])
|
|
: null;
|
|
|
|
if (self::isUnsafeExecutable($type, $capability, $auditEvent, $requiresConfirmation, $operationRunType)) {
|
|
$type = self::fallbackType($rawKind, $url);
|
|
$actionName = null;
|
|
$capability = null;
|
|
$requiresConfirmation = false;
|
|
$auditEvent = null;
|
|
$operationRunType = null;
|
|
}
|
|
|
|
$kind = self::kindFromType($type, $rawKind);
|
|
$icon = is_string($action['icon'] ?? null) && trim((string) $action['icon']) !== ''
|
|
? trim((string) $action['icon'])
|
|
: self::iconForType($type);
|
|
$disabledReason = is_string($action['disabled_reason'] ?? null) && trim((string) $action['disabled_reason']) !== ''
|
|
? trim((string) $action['disabled_reason'])
|
|
: null;
|
|
|
|
return [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'type' => $type,
|
|
'url' => $url,
|
|
'icon' => $icon,
|
|
'kind' => $kind,
|
|
'action_name' => $actionName,
|
|
'capability' => $capability,
|
|
'requires_confirmation' => $requiresConfirmation,
|
|
'audit_event' => $auditEvent,
|
|
'operation_run_type' => $operationRunType,
|
|
'disabled_reason' => $disabledReason,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* key:string,
|
|
* label:string,
|
|
* type:string,
|
|
* url:null,
|
|
* icon:string,
|
|
* kind:string,
|
|
* action_name:null,
|
|
* capability:null,
|
|
* requires_confirmation:false,
|
|
* audit_event:null,
|
|
* operation_run_type:null,
|
|
* disabled_reason:?string
|
|
* }
|
|
*/
|
|
public static function none(string $key, string $label, ?string $disabledReason = null): array
|
|
{
|
|
return [
|
|
'key' => $key,
|
|
'label' => $label,
|
|
'type' => self::TYPE_NONE,
|
|
'url' => null,
|
|
'icon' => self::iconForType(self::TYPE_NONE),
|
|
'kind' => 'none',
|
|
'action_name' => null,
|
|
'capability' => null,
|
|
'requires_confirmation' => false,
|
|
'audit_event' => null,
|
|
'operation_run_type' => null,
|
|
'disabled_reason' => $disabledReason,
|
|
];
|
|
}
|
|
|
|
private static function typeFromKind(?string $kind, ?string $url): string
|
|
{
|
|
return match ($kind) {
|
|
'download' => self::TYPE_DOWNLOAD,
|
|
'disclosure' => self::TYPE_DISCLOSURE,
|
|
'none' => self::TYPE_NONE,
|
|
default => $url !== null ? self::TYPE_NAVIGATION : self::TYPE_NONE,
|
|
};
|
|
}
|
|
|
|
private static function fallbackType(?string $kind, ?string $url): string
|
|
{
|
|
return match (true) {
|
|
$kind === 'download' => self::TYPE_DOWNLOAD,
|
|
$kind === 'disclosure' => self::TYPE_DISCLOSURE,
|
|
$url !== null => self::TYPE_NAVIGATION,
|
|
default => self::TYPE_NONE,
|
|
};
|
|
}
|
|
|
|
private static function kindFromType(string $type, ?string $kind): string
|
|
{
|
|
if (is_string($kind) && $kind !== '') {
|
|
return $kind;
|
|
}
|
|
|
|
return match ($type) {
|
|
self::TYPE_DOWNLOAD => 'download',
|
|
self::TYPE_DISCLOSURE => 'disclosure',
|
|
self::TYPE_NONE => 'none',
|
|
default => 'environment_link',
|
|
};
|
|
}
|
|
|
|
private static function iconForType(string $type): string
|
|
{
|
|
return match ($type) {
|
|
self::TYPE_DOWNLOAD => 'heroicon-o-arrow-down-tray',
|
|
self::TYPE_DISCLOSURE => 'heroicon-o-information-circle',
|
|
self::TYPE_NONE => 'heroicon-o-minus-circle',
|
|
default => 'heroicon-o-arrow-top-right-on-square',
|
|
};
|
|
}
|
|
|
|
private static function isUnsafeExecutable(
|
|
string $type,
|
|
?string $capability,
|
|
?string $auditEvent,
|
|
bool $requiresConfirmation,
|
|
?string $operationRunType,
|
|
): bool {
|
|
if (! in_array($type, [self::TYPE_DOMAIN_ACTION, self::TYPE_OPERATION_ACTION], true)) {
|
|
return false;
|
|
}
|
|
|
|
if ($capability === null || $auditEvent === null) {
|
|
return true;
|
|
}
|
|
|
|
if (! $requiresConfirmation) {
|
|
return true;
|
|
}
|
|
|
|
return $type === self::TYPE_OPERATION_ACTION && $operationRunType === null;
|
|
}
|
|
}
|