>, * resource: array>, * uri: array>, * generic: array> * } */ private array $hashValues = [ 'inline' => [], 'resource' => [], 'uri' => [], 'generic' => [], ]; public function __construct(private readonly ResourceHashCollection $resourceHashCollection) {} /** * Computes a SHA-256 hash of the given inline content and stores it for the directive. */ public function addInlineHash(Directive $directive, string $content): void { $this->addHashValue($directive, HashValue::hash($content), 'inline'); } /** * Lazily computes a hash from a local file identified by an EXT: path or absolute path. * No-op if the file cannot be read. */ public function addResourceHash(Directive $directive, string|UriInterface|StaticResourceInterface $resource): void { if (is_string($resource)) { $resource = $this->resourceHashCollection->resolveResourceValue($resource); if ($resource === null) { return; } } $hashValue = $this->resourceHashCollection->fetchResourceHash($resource); if ($hashValue === null) { return; } if ($resource instanceof UriInterface || $resource instanceof UriResource) { $this->addHashValue($directive, $hashValue, 'uri'); } else { $this->addHashValue($directive, $hashValue, 'resource'); } } /** * Stores an already-computed HashValue (e.g. parsed from an `integrity` attribute). */ public function addGenericHashValue(Directive $directive, HashValue|string $hashValue): void { if (is_string($hashValue)) { $hashValue = $this->convertHashValue($hashValue); } if ($hashValue !== null) { $this->addHashValue($directive, $hashValue, 'generic'); } } /** * Converts all stored hashes into MutationCollection instances that can be applied to the CSP. * Hash values from all types are merged per directive. * * @return list */ public function asMutationCollections(): array { $byDirective = []; foreach ($this->hashValues as $typeHashValues) { foreach ($typeHashValues as $directiveName => $hashValues) { $byDirective[$directiveName] = array_merge($byDirective[$directiveName] ?? [], $hashValues); } } $collections = []; foreach ($byDirective as $directiveName => $hashValues) { $directive = Directive::from($directiveName); // filter out duplicates $stringHashValues = array_unique(array_map(strval(...), $hashValues)); $hashValues = array_map(HashValue::fromString(...), $stringHashValues); // convert to CSP mutation $mutations = array_map( static fn(HashValue $hash): Mutation => new Mutation(MutationMode::Extend, $directive, $hash), $hashValues ); $collections[] = new MutationCollection(...$mutations); } return $collections; } public function isEmpty(): bool { foreach ($this->hashValues as $typeHashValues) { if ($typeHashValues !== []) { return false; } } return true; } public function countInlineHashValues(?string $aspect = null): int { if ($aspect === null) { return array_sum( array_map(count(...), $this->hashValues['inline']) ); } return count($this->hashValues['inline'][$aspect] ?? []); } public function jsonSerialize(): array { $serialized = []; foreach ($this->hashValues as $type => $typeHashValues) { foreach ($typeHashValues as $directiveName => $hashValues) { $serialized[$type][$directiveName] = array_map( static fn(HashValue $hashValue): string => $hashValue->export(), $hashValues ); } } return $serialized; } /** * Restores hash values from a previously serialized (cached) state. */ public function updateFromJson(array $data): void { foreach ($data as $type => $typeHashValues) { foreach ($typeHashValues as $directiveName => $hashItems) { $directive = Directive::from($directiveName); foreach ($hashItems as $item) { $this->addHashValue($directive, HashValue::fromString($item), $type); } } } } private function addHashValue(Directive $directive, HashValue $hashValue, string $type): void { $this->hashValues[$type][$directive->value] ??= []; $this->hashValues[$type][$directive->value][] = $hashValue; } private function convertHashValue(string $hashValue): ?HashValue { try { return HashValue::fromString($hashValue); } catch (\LogicException) { // hash format not recognized, skip return null; } } }