Implementing report profiles and disclosure policy as per spec 357. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #428
180 lines
7.6 KiB
PHP
180 lines
7.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPacks;
|
|
|
|
final class ReportDisclosurePolicy
|
|
{
|
|
public const string PROOF_VERIFIED = 'verified';
|
|
|
|
public const string PROOF_ASSUMED = 'assumed';
|
|
|
|
public const string PROOF_MISSING = 'missing';
|
|
|
|
public const string PROOF_UNKNOWN = 'unknown';
|
|
|
|
public const string PROOF_NOT_APPLICABLE = 'not_applicable';
|
|
|
|
/**
|
|
* @param array<string, mixed> $profile
|
|
* @param array<string, mixed> $readiness
|
|
* @param array<string, mixed> $metadata
|
|
* @return array{
|
|
* mandatory_disclosures:list<array{key:string,label:string,summary:string,proof_state:string}>,
|
|
* warnings:list<array{key:string,label:string,summary:string}>,
|
|
* blocking_reasons:list<array{key:string,label:string,summary:string}>,
|
|
* proof_states:array{audience_boundary:string,evidence_basis:string,protected_values:string,non_certification:string},
|
|
* show_section_appendix:bool,
|
|
* show_technical_details:bool
|
|
* }
|
|
*/
|
|
public static function evaluate(array $profile, array $readiness, array $metadata = []): array
|
|
{
|
|
$isCustomerFacing = (bool) ($profile['is_customer_facing'] ?? false);
|
|
$containsPii = (bool) ($readiness['contains_pii'] ?? false);
|
|
$protectedValuesHidden = (bool) ($readiness['protected_values_hidden'] ?? false);
|
|
$disclosurePresent = (bool) ($readiness['disclosure_present'] ?? false);
|
|
$displayedDisclosure = self::plainText(
|
|
$metadata['non_certification_disclosure'] ?? null,
|
|
__('localization.review.non_certification_disclosure_text'),
|
|
);
|
|
|
|
$proofStates = [
|
|
'audience_boundary' => self::PROOF_VERIFIED,
|
|
'evidence_basis' => self::evidenceBasisProofState((string) ($readiness['evidence_completeness_state'] ?? '')),
|
|
'protected_values' => self::protectedValuesProofState(
|
|
isCustomerFacing: $isCustomerFacing,
|
|
containsPii: $containsPii,
|
|
protectedValuesHidden: $protectedValuesHidden,
|
|
),
|
|
'non_certification' => $disclosurePresent
|
|
? self::PROOF_ASSUMED
|
|
: self::PROOF_MISSING,
|
|
];
|
|
|
|
$blockingReasons = [];
|
|
|
|
if ($isCustomerFacing && $containsPii) {
|
|
$blockingReasons[] = [
|
|
'key' => 'customer_profile_internal_only',
|
|
'label' => __('localization.review.report_disclosure_customer_profile_internal_only'),
|
|
'summary' => __('localization.review.report_disclosure_customer_profile_internal_only_summary'),
|
|
];
|
|
}
|
|
|
|
$warnings = [];
|
|
|
|
if ((bool) ($profile['is_fallback'] ?? false)) {
|
|
$warnings[] = [
|
|
'key' => 'profile_fallback',
|
|
'label' => __('localization.review.report_profile_fallback_notice'),
|
|
'summary' => __('localization.review.report_profile_fallback_summary'),
|
|
];
|
|
}
|
|
|
|
if ($isCustomerFacing && (string) ($readiness['customer_safe_state'] ?? '') !== ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY) {
|
|
$warnings[] = [
|
|
'key' => 'customer_profile_requires_review',
|
|
'label' => __('localization.review.report_external_sharing_warning'),
|
|
'summary' => __('localization.review.report_disclosure_customer_profile_requires_review'),
|
|
];
|
|
}
|
|
|
|
if ($proofStates['non_certification'] === self::PROOF_MISSING) {
|
|
$warnings[] = [
|
|
'key' => 'non_certification_missing',
|
|
'label' => __('localization.review.non_certification_disclosure'),
|
|
'summary' => __('localization.review.report_disclosure_non_certification_missing'),
|
|
];
|
|
}
|
|
|
|
$showDetailedContent = ! ($isCustomerFacing && $containsPii);
|
|
|
|
return [
|
|
'mandatory_disclosures' => [
|
|
[
|
|
'key' => 'audience_boundary',
|
|
'label' => __('localization.review.report_disclosure_audience_boundary'),
|
|
'summary' => __('localization.review.report_disclosure_audience_boundary_summary', [
|
|
'audience' => (string) ($profile['audience_label'] ?? __('localization.review.unavailable')),
|
|
]),
|
|
'proof_state' => $proofStates['audience_boundary'],
|
|
],
|
|
[
|
|
'key' => 'evidence_basis',
|
|
'label' => __('localization.review.report_disclosure_evidence_basis'),
|
|
'summary' => match ($proofStates['evidence_basis']) {
|
|
self::PROOF_VERIFIED => __('localization.review.report_disclosure_evidence_verified'),
|
|
self::PROOF_MISSING => __('localization.review.report_disclosure_evidence_missing'),
|
|
default => __('localization.review.report_disclosure_evidence_unknown'),
|
|
},
|
|
'proof_state' => $proofStates['evidence_basis'],
|
|
],
|
|
[
|
|
'key' => 'protected_values',
|
|
'label' => __('localization.review.report_disclosure_protected_values'),
|
|
'summary' => match ($proofStates['protected_values']) {
|
|
self::PROOF_ASSUMED => __('localization.review.report_disclosure_protected_values_assumed'),
|
|
self::PROOF_NOT_APPLICABLE => __('localization.review.report_disclosure_protected_values_not_applicable'),
|
|
self::PROOF_MISSING => __('localization.review.report_disclosure_protected_values_missing'),
|
|
default => __('localization.review.report_disclosure_protected_values_unknown'),
|
|
},
|
|
'proof_state' => $proofStates['protected_values'],
|
|
],
|
|
[
|
|
'key' => 'non_certification',
|
|
'label' => __('localization.review.non_certification_disclosure'),
|
|
'summary' => $displayedDisclosure,
|
|
'proof_state' => $proofStates['non_certification'],
|
|
],
|
|
],
|
|
'warnings' => $warnings,
|
|
'blocking_reasons' => $blockingReasons,
|
|
'proof_states' => $proofStates,
|
|
'show_section_appendix' => (bool) ($profile['show_section_appendix'] ?? false) && $showDetailedContent,
|
|
'show_technical_details' => (bool) ($profile['show_technical_details'] ?? false) && $showDetailedContent,
|
|
];
|
|
}
|
|
|
|
private static function evidenceBasisProofState(string $evidenceCompletenessState): string
|
|
{
|
|
return match ($evidenceCompletenessState) {
|
|
'complete' => self::PROOF_VERIFIED,
|
|
'missing', 'partial', 'stale' => self::PROOF_MISSING,
|
|
default => self::PROOF_UNKNOWN,
|
|
};
|
|
}
|
|
|
|
private static function protectedValuesProofState(
|
|
bool $isCustomerFacing,
|
|
bool $containsPii,
|
|
bool $protectedValuesHidden,
|
|
): string {
|
|
if (! $isCustomerFacing) {
|
|
return self::PROOF_NOT_APPLICABLE;
|
|
}
|
|
|
|
if ($containsPii || ! $protectedValuesHidden) {
|
|
return self::PROOF_MISSING;
|
|
}
|
|
|
|
return self::PROOF_ASSUMED;
|
|
}
|
|
|
|
private static function plainText(mixed $value, string $fallback): string
|
|
{
|
|
if (! is_scalar($value) && $value !== null) {
|
|
return $fallback;
|
|
}
|
|
|
|
$text = preg_replace('/\s+/', ' ', trim((string) $value));
|
|
|
|
if (! is_string($text) || $text === '') {
|
|
return $fallback;
|
|
}
|
|
|
|
return str_starts_with($text, 'localization.') ? $fallback : $text;
|
|
}
|
|
}
|