‪TYPO3CMS  ‪main
FilePersistenceSlot.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
31 
38 {
39  public const ‪COMMAND_FILE_ADD = 'fileAdd';
40  public const ‪COMMAND_FILE_CREATE = 'fileCreate';
41  public const ‪COMMAND_FILE_MOVE = 'fileMove';
42  public const ‪COMMAND_FILE_RENAME = 'fileRename';
43  public const ‪COMMAND_FILE_REPLACE = 'fileReplace';
44  public const ‪COMMAND_FILE_SET_CONTENTS = 'fileSetContents';
45 
49  protected ‪$definedInvocations = [];
50 
54  protected ‪$allowedInvocations = [];
55 
56  public function ‪__construct(private readonly ‪HashService $hashService) {}
57 
58  public function ‪getContentSignature(string $content): string
59  {
60  return $this->hashService->hmac($content, self::class);
61  }
62 
73  public function ‪defineInvocation(string $command, bool $type = null)
74  {
75  $this->definedInvocations[$command] = $type;
76  if ($type === null) {
77  unset($this->definedInvocations[$command]);
78  }
79  }
80 
88  public function ‪allowInvocation(
89  string $command,
90  string $combinedFileIdentifier,
91  string $contentSignature = null
92  ): bool {
93  $index = $this->‪searchAllowedInvocation(
94  $command,
95  $combinedFileIdentifier,
96  $contentSignature
97  );
98 
99  if ($index !== null) {
100  return false;
101  }
102 
103  $this->allowedInvocations[] = [
104  'command' => $command,
105  'combinedFileIdentifier' => $combinedFileIdentifier,
106  'contentSignature' => $contentSignature,
107  ];
108 
109  return true;
110  }
111 
112  #[AsEventListener('form-framework/creation')]
113  public function ‪onPreFileCreate(‪BeforeFileCreatedEvent $event): void
114  {
115  $combinedFileIdentifier = $this->‪buildCombinedIdentifier(
116  $event->‪getFolder(),
117  $event->‪getFileName()
118  );
119 
120  $this->‪assertFileName(
121  self::COMMAND_FILE_CREATE,
122  $combinedFileIdentifier
123  );
124  }
125 
126  #[AsEventListener('form-framework/add')]
127  public function ‪onPreFileAdd(‪BeforeFileAddedEvent $event): void
128  {
129  $combinedFileIdentifier = $this->‪buildCombinedIdentifier(
130  $event->‪getTargetFolder(),
131  $event->‪getFileName()
132  );
133  // while assertFileName below also checks if it's a form definition
134  // we want an early return here to get rid of the file_get_contents
135  // below which would be triggered on every file add command otherwise
136  if (!$this->‪isFormDefinition($combinedFileIdentifier)) {
137  return;
138  }
139  $this->‪assertFileName(
140  self::COMMAND_FILE_ADD,
141  $combinedFileIdentifier,
142  (string)file_get_contents($event->‪getSourceFilePath())
143  );
144  }
145 
146  #[AsEventListener('form-framework/rename')]
147  public function ‪onPreFileRename(‪BeforeFileRenamedEvent $event): void
148  {
149  $combinedFileIdentifier = $this->‪buildCombinedIdentifier(
150  $event->‪getFile()->getParentFolder(),
151  $event->‪getTargetFileName() ?? ''
152  );
153 
154  $this->‪assertFileName(
155  self::COMMAND_FILE_RENAME,
156  $combinedFileIdentifier
157  );
158  }
159 
160  #[AsEventListener('form-framework/replace')]
161  public function ‪onPreFileReplace(‪BeforeFileReplacedEvent $event): void
162  {
163  $combinedFileIdentifier = $this->‪buildCombinedIdentifier(
164  $event->‪getFile()->getParentFolder(),
165  $event->‪getFile()->getName()
166  );
167 
168  $this->‪assertFileName(
169  self::COMMAND_FILE_REPLACE,
170  $combinedFileIdentifier
171  );
172  }
173 
174  #[AsEventListener('form-framework/move')]
175  public function ‪onPreFileMove(‪BeforeFileMovedEvent $event): void
176  {
177  // Skip check, in case file extension would not change during this
178  // command. In case e.g. "file.txt" shall be renamed to "file.form.yaml"
179  // the invocation still has to be granted.
180  // Any file moved to a recycle folder is accepted as well.
181  if ($this->‪isFormDefinition($event->‪getFile()->getIdentifier())
182  && $this->isFormDefinition($event->‪getTargetFileName())
183  || $this->isRecycleFolder($event->‪getFolder())) {
184  return;
185  }
186 
187  $combinedFileIdentifier = $this->‪buildCombinedIdentifier(
188  $event->‪getFolder(),
189  $event->‪getTargetFileName()
190  );
191 
192  $this->‪assertFileName(
193  self::COMMAND_FILE_MOVE,
194  $combinedFileIdentifier
195  );
196  }
197 
198  #[AsEventListener('form-framework/update-content')]
199  public function ‪onPreFileSetContents(‪BeforeFileContentsSetEvent $event): void
200  {
201  $combinedFileIdentifier = $this->‪buildCombinedIdentifier(
202  $event->‪getFile()->getParentFolder(),
203  $event->‪getFile()->getName()
204  );
205 
206  $this->‪assertFileName(
207  self::COMMAND_FILE_SET_CONTENTS,
208  $combinedFileIdentifier,
209  $event->‪getContent()
210  );
211  }
212 
216  protected function ‪assertFileName(
217  string $command,
218  string $combinedFileIdentifier,
219  string $content = null
220  ): void {
221  if (!$this->‪isFormDefinition($combinedFileIdentifier)) {
222  return;
223  }
224 
225  $definedInvocation = $this->definedInvocations[$command] ?? null;
226  // whitelisted command
227  if ($definedInvocation === true) {
228  return;
229  }
230  // blacklisted command
231  if ($definedInvocation === false) {
232  throw new FormDefinitionPersistenceException(
233  sprintf(
234  'Persisting form definition "%s" is denied',
235  $combinedFileIdentifier
236  ),
237  1530281201
238  );
239  }
240 
241  $contentSignature = null;
242  if ($content !== null) {
243  $contentSignature = $this->‪getContentSignature($content);
244  }
245  $allowedInvocationIndex = $this->‪searchAllowedInvocation(
246  $command,
247  $combinedFileIdentifier,
248  $contentSignature
249  );
250 
251  if ($allowedInvocationIndex === null) {
252  throw new FormDefinitionPersistenceException(
253  sprintf(
254  'Persisting form definition "%s" is denied',
255  $combinedFileIdentifier
256  ),
257  1530281202
258  );
259  }
260  unset($this->allowedInvocations[$allowedInvocationIndex]);
261  }
262 
266  protected function ‪searchAllowedInvocation(
267  string $command,
268  string $combinedFileIdentifier,
269  string $contentSignature = null
270  ): ?int {
271  foreach ($this->allowedInvocations as $index => $allowedInvocation) {
272  if (
273  $command === $allowedInvocation['command']
274  && $combinedFileIdentifier === $allowedInvocation['combinedFileIdentifier']
275  && $contentSignature === $allowedInvocation['contentSignature']
276  ) {
277  return $index;
278  }
279  }
280  return null;
281  }
282 
283  protected function ‪buildCombinedIdentifier(‪FolderInterface $folder, string $fileName): string
284  {
285  return sprintf(
286  '%d:%s%s',
287  $folder->‪getStorage()->getUid(),
288  $folder->‪getIdentifier(),
289  $fileName
290  );
291  }
292 
293  protected function ‪isFormDefinition(string ‪$identifier): bool
294  {
295  return str_ends_with(
298  );
299  }
300 
301  protected function ‪isRecycleFolder(‪FolderInterface $folder): bool
302  {
303  $role = $folder->‪getStorage()->getRole($folder);
304  return $role === ‪FolderInterface::ROLE_RECYCLER;
305  }
306 }
‪TYPO3\CMS\Core\Resource\ResourceInterface\getIdentifier
‪getIdentifier()
‪TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent
Definition: BeforeFileCreatedEvent.php:29
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\isFormDefinition
‪isFormDefinition(string $identifier)
Definition: FilePersistenceSlot.php:291
‪TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent
Definition: BeforeFileReplacedEvent.php:27
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\__construct
‪__construct(private readonly HashService $hashService)
Definition: FilePersistenceSlot.php:54
‪TYPO3\CMS\Core\Attribute\AsEventListener
Definition: AsEventListener.php:25
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\onPreFileMove
‪onPreFileMove(BeforeFileMovedEvent $event)
Definition: FilePersistenceSlot.php:173
‪TYPO3\CMS\Core\Resource\ResourceInterface\getStorage
‪getStorage()
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\COMMAND_FILE_SET_CONTENTS
‪const COMMAND_FILE_SET_CONTENTS
Definition: FilePersistenceSlot.php:44
‪TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent
Definition: BeforeFileRenamedEvent.php:27
‪TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent\getTargetFileName
‪getTargetFileName()
Definition: BeforeFileMovedEvent.php:45
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\onPreFileSetContents
‪onPreFileSetContents(BeforeFileContentsSetEvent $event)
Definition: FilePersistenceSlot.php:197
‪TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent
Definition: BeforeFileMovedEvent.php:28
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\onPreFileAdd
‪onPreFileAdd(BeforeFileAddedEvent $event)
Definition: FilePersistenceSlot.php:125
‪TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent\getFolder
‪getFolder()
Definition: BeforeFileCreatedEvent.php:37
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\COMMAND_FILE_MOVE
‪const COMMAND_FILE_MOVE
Definition: FilePersistenceSlot.php:41
‪TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent\getFile
‪getFile()
Definition: BeforeFileRenamedEvent.php:30
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\COMMAND_FILE_CREATE
‪const COMMAND_FILE_CREATE
Definition: FilePersistenceSlot.php:40
‪TYPO3\CMS\Core\Resource\Event\BeforeFileCreatedEvent\getFileName
‪getFileName()
Definition: BeforeFileCreatedEvent.php:32
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\COMMAND_FILE_ADD
‪const COMMAND_FILE_ADD
Definition: FilePersistenceSlot.php:39
‪TYPO3\CMS\Form\Slot
Definition: FilePersistenceSlot.php:18
‪TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManager
Definition: FormPersistenceManager.php:62
‪TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent\getContent
‪getContent()
Definition: BeforeFileContentsSetEvent.php:36
‪TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent\getSourceFilePath
‪getSourceFilePath()
Definition: BeforeFileAddedEvent.php:49
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\assertFileName
‪assertFileName(string $command, string $combinedFileIdentifier, string $content=null)
Definition: FilePersistenceSlot.php:214
‪TYPO3\CMS\Core\Resource\Event\BeforeFileReplacedEvent\getFile
‪getFile()
Definition: BeforeFileReplacedEvent.php:30
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\getContentSignature
‪getContentSignature(string $content)
Definition: FilePersistenceSlot.php:56
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\isRecycleFolder
‪isRecycleFolder(FolderInterface $folder)
Definition: FilePersistenceSlot.php:299
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\defineInvocation
‪defineInvocation(string $command, bool $type=null)
Definition: FilePersistenceSlot.php:71
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\$definedInvocations
‪array $definedInvocations
Definition: FilePersistenceSlot.php:48
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\onPreFileRename
‪onPreFileRename(BeforeFileRenamedEvent $event)
Definition: FilePersistenceSlot.php:145
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\allowInvocation
‪allowInvocation(string $command, string $combinedFileIdentifier, string $contentSignature=null)
Definition: FilePersistenceSlot.php:86
‪TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent\getFileName
‪getFileName()
Definition: BeforeFileAddedEvent.php:39
‪TYPO3\CMS\Core\Resource\Event\BeforeFileRenamedEvent\getTargetFileName
‪getTargetFileName()
Definition: BeforeFileRenamedEvent.php:35
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot
Definition: FilePersistenceSlot.php:38
‪TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent\getFolder
‪getFolder()
Definition: BeforeFileMovedEvent.php:40
‪TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent
Definition: BeforeFileAddedEvent.php:30
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\onPreFileCreate
‪onPreFileCreate(BeforeFileCreatedEvent $event)
Definition: FilePersistenceSlot.php:111
‪TYPO3\CMS\Form\Slot\FormDefinitionPersistenceException
Definition: FormDefinitionPersistenceException.php:23
‪TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent\getFile
‪getFile()
Definition: BeforeFileContentsSetEvent.php:31
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\COMMAND_FILE_REPLACE
‪const COMMAND_FILE_REPLACE
Definition: FilePersistenceSlot.php:43
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪TYPO3\CMS\Core\Resource\Event\BeforeFileMovedEvent\getFile
‪getFile()
Definition: BeforeFileMovedEvent.php:35
‪TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent\getTargetFolder
‪getTargetFolder()
Definition: BeforeFileAddedEvent.php:54
‪TYPO3\CMS\Core\Resource\FolderInterface\ROLE_RECYCLER
‪const ROLE_RECYCLER
Definition: FolderInterface.php:29
‪TYPO3\CMS\Core\Resource\FolderInterface
Definition: FolderInterface.php:24
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\onPreFileReplace
‪onPreFileReplace(BeforeFileReplacedEvent $event)
Definition: FilePersistenceSlot.php:159
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\searchAllowedInvocation
‪searchAllowedInvocation(string $command, string $combinedFileIdentifier, string $contentSignature=null)
Definition: FilePersistenceSlot.php:264
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\COMMAND_FILE_RENAME
‪const COMMAND_FILE_RENAME
Definition: FilePersistenceSlot.php:42
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\buildCombinedIdentifier
‪buildCombinedIdentifier(FolderInterface $folder, string $fileName)
Definition: FilePersistenceSlot.php:281
‪TYPO3\CMS\Form\Slot\FilePersistenceSlot\$allowedInvocations
‪array $allowedInvocations
Definition: FilePersistenceSlot.php:52
‪TYPO3\CMS\Core\Crypto\HashService
Definition: HashService.php:27
‪TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManager\FORM_DEFINITION_FILE_EXTENSION
‪const FORM_DEFINITION_FILE_EXTENSION
Definition: FormPersistenceManager.php:63
‪TYPO3\CMS\Core\Resource\Event\BeforeFileContentsSetEvent
Definition: BeforeFileContentsSetEvent.php:28
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37