hashService->hmac($content, self::class); } /** * Allows invocation for a particular combination of command and file * identifier. Commands providing new content have to submit a HMAC * signature on the content as well. * * @see getContentSignature */ public function allowInvocation(string $command, string $combinedFileIdentifier, ?string $contentSignature = null): bool { $index = $this->searchAllowedInvocation($command, $combinedFileIdentifier, $contentSignature); if ($index !== null) { return false; } $this->allowedInvocations[] = [ 'command' => $command, 'combinedFileIdentifier' => $combinedFileIdentifier, 'contentSignature' => $contentSignature, ]; return true; } #[AsEventListener('form-framework/creation')] public function onPreFileCreate(BeforeFileCreatedEvent $event): void { $combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFolder(), $event->getFileName()); $this->assertFileName(self::COMMAND_FILE_CREATE, $combinedFileIdentifier); } #[AsEventListener('form-framework/add')] public function onPreFileAdd(BeforeFileAddedEvent $event): void { $combinedFileIdentifier = $this->buildCombinedIdentifier($event->getTargetFolder(), $event->getFileName()); // While assertFileName() below also checks if it's a form definition // we want an early return here to not file_get_contents() below which // would be triggered on every file add() command otherwise. if (!$this->isFormDefinition($combinedFileIdentifier)) { return; } $this->assertFileName(self::COMMAND_FILE_ADD, $combinedFileIdentifier, (string)file_get_contents($event->getSourceFilePath())); } #[AsEventListener('form-framework/rename')] public function onPreFileRename(BeforeFileRenamedEvent $event): void { $combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFile()->getParentFolder(), $event->getTargetFileName() ?? ''); $this->assertFileName(self::COMMAND_FILE_RENAME, $combinedFileIdentifier); } #[AsEventListener('form-framework/replace')] public function onPreFileReplace(BeforeFileReplacedEvent $event): void { $combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFile()->getParentFolder(), $event->getFile()->getName()); $this->assertFileName(self::COMMAND_FILE_REPLACE, $combinedFileIdentifier); } #[AsEventListener('form-framework/move')] public function onPreFileMove(BeforeFileMovedEvent $event): void { // Skip check, in case file extension would not change during this // command. In case e.g. "file.txt" shall be renamed to "file.form.yaml" // the invocation still has to be granted. // Any file moved to a recycle folder is accepted as well. if ($this->isFormDefinition($event->getFile()->getIdentifier()) && $this->isFormDefinition($event->getTargetFileName()) || $this->isRecycleFolder($event->getFolder())) { return; } $combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFolder(), $event->getTargetFileName()); $this->assertFileName(self::COMMAND_FILE_MOVE, $combinedFileIdentifier); } #[AsEventListener('form-framework/update-content')] public function onPreFileSetContents(BeforeFileContentsSetEvent $event): void { $combinedFileIdentifier = $this->buildCombinedIdentifier($event->getFile()->getParentFolder(), $event->getFile()->getName()); $this->assertFileName(self::COMMAND_FILE_SET_CONTENTS, $combinedFileIdentifier, $event->getContent()); } /** * @throws FormDefinitionPersistenceException */ private function assertFileName(string $command, string $combinedFileIdentifier, ?string $content = null): void { if (!$this->isFormDefinition($combinedFileIdentifier)) { return; } $contentSignature = null; if ($content !== null) { $contentSignature = $this->getContentSignature($content); } $allowedInvocationIndex = $this->searchAllowedInvocation($command, $combinedFileIdentifier, $contentSignature); if ($allowedInvocationIndex === null) { throw new FormDefinitionPersistenceException( sprintf('Persisting form definition "%s" is denied', $combinedFileIdentifier), 1530281202 ); } unset($this->allowedInvocations[$allowedInvocationIndex]); } private function searchAllowedInvocation(string $command, string $combinedFileIdentifier, ?string $contentSignature = null): ?int { foreach ($this->allowedInvocations as $index => $allowedInvocation) { if ($command === $allowedInvocation['command'] && $combinedFileIdentifier === $allowedInvocation['combinedFileIdentifier'] && $contentSignature === $allowedInvocation['contentSignature'] ) { return $index; } } return null; } private function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string { return sprintf('%d:%s%s', $folder->getStorage()->getUid(), $folder->getIdentifier(), $fileName); } private function isFormDefinition(string $identifier): bool { return str_ends_with( mb_strtolower($identifier), FormPersistenceManagerInterface::FORM_DEFINITION_FILE_EXTENSION ); } private function isRecycleFolder(FolderInterface $folder): bool { $role = $folder->getStorage()->getRole($folder); return $role === FolderInterface::ROLE_RECYCLER; } }