TYPO3 CMS  TYPO3_8-7
FilePersistenceSlot.php
Go to the documentation of this file.
1 <?php
2 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 
24 
29 {
30  const COMMAND_FILE_ADD = 'fileAdd';
31  const COMMAND_FILE_CREATE = 'fileCreate';
32  const COMMAND_FILE_MOVE = 'fileMove';
33  const COMMAND_FILE_RENAME = 'fileRename';
34  const COMMAND_FILE_REPLACE = 'fileReplace';
35  const COMMAND_FILE_SET_CONTENTS = 'fileSetContents';
36 
40  protected $definedInvocations = [];
41 
45  protected $allowedInvocations = [];
46 
51  public function getContentSignature(string $content): string
52  {
53  return GeneralUtility::hmac($content);
54  }
55 
66  public function defineInvocation(string $command, bool $type = null)
67  {
68  $this->definedInvocations[$command] = $type;
69  if ($type === null) {
70  unset($this->definedInvocations[$command]);
71  }
72  }
73 
85  public function allowInvocation(
86  string $command,
87  string $combinedFileIdentifier,
88  string $contentSignature = null
89  ): bool {
90  $index = $this->searchAllowedInvocation(
91  $command,
92  $combinedFileIdentifier,
93  $contentSignature
94  );
95 
96  if ($index !== null) {
97  return false;
98  }
99 
100  $this->allowedInvocations[] = [
101  'command' => $command,
102  'combinedFileIdentifier' => $combinedFileIdentifier,
103  'contentSignature' => $contentSignature,
104  ];
105 
106  return true;
107  }
108 
113  public function onPreFileCreate(string $fileName, FolderInterface $targetFolder)
114  {
115  $combinedFileIdentifier = $this->buildCombinedIdentifier(
116  $targetFolder,
117  $fileName
118  );
119 
120  $this->assertFileName(
121  self::COMMAND_FILE_CREATE,
122  $combinedFileIdentifier
123  );
124  }
125 
131  public function onPreFileAdd(
132  string $targetFileName,
133  FolderInterface $targetFolder,
134  string $sourceFilePath
135  ) {
136  $combinedFileIdentifier = $this->buildCombinedIdentifier(
137  $targetFolder,
138  $targetFileName
139  );
140  $this->assertFileName(
141  self::COMMAND_FILE_ADD,
142  $combinedFileIdentifier,
143  file_get_contents($sourceFilePath)
144  );
145  }
146 
151  public function onPreFileRename(FileInterface $file, string $targetFileName)
152  {
153  $combinedFileIdentifier = $this->buildCombinedIdentifier(
154  $file->getParentFolder(),
155  $targetFileName
156  );
157 
158  $this->assertFileName(
159  self::COMMAND_FILE_RENAME,
160  $combinedFileIdentifier
161  );
162  }
163 
168  public function onPreFileReplace(FileInterface $file, string $localFilePath)
169  {
170  $combinedFileIdentifier = $this->buildCombinedIdentifier(
171  $file->getParentFolder(),
172  $file->getName()
173  );
174 
175  $this->assertFileName(
176  self::COMMAND_FILE_REPLACE,
177  $combinedFileIdentifier
178  );
179  }
180 
186  public function onPreFileMove(FileInterface $file, FolderInterface $targetFolder, string $targetFileName)
187  {
188  // Skip check, in case file extension would not change during this
189  // command. In case e.g. "file.txt" shall be renamed to "file.form.yaml"
190  // the invocation still has to be granted.
191  // Any file moved to a recycle folder is accepted as well.
192  if ($this->isFormDefinition($file->getIdentifier())
193  && $this->isFormDefinition($targetFileName)
194  || $this->isRecycleFolder($targetFolder)) {
195  return;
196  }
197 
198  $combinedFileIdentifier = $this->buildCombinedIdentifier(
199  $targetFolder,
200  $targetFileName
201  );
202 
203  $this->assertFileName(
204  self::COMMAND_FILE_MOVE,
205  $combinedFileIdentifier
206  );
207  }
208 
213  public function onPreFileSetContents(FileInterface $file, $content = null)
214  {
215  $combinedFileIdentifier = $this->buildCombinedIdentifier(
216  $file->getParentFolder(),
217  $file->getName()
218  );
219 
220  $this->assertFileName(
221  self::COMMAND_FILE_SET_CONTENTS,
222  $combinedFileIdentifier,
223  $content
224  );
225  }
226 
233  protected function assertFileName(
234  string $command,
235  string $combinedFileIdentifier,
236  string $content = null
237  ) {
238  if (!$this->isFormDefinition($combinedFileIdentifier)) {
239  return;
240  }
241 
242  $definedInvocation = $this->definedInvocations[$command] ?? null;
243  // whitelisted command
244  if ($definedInvocation === true) {
245  return;
246  }
247  // blacklisted command
248  if ($definedInvocation === false) {
250  sprintf(
251  'Persisting form definition "%s" is denied',
252  $combinedFileIdentifier
253  ),
254  1530281201
255  );
256  }
257 
258  $contentSignature = null;
259  if ($content !== null) {
260  $contentSignature = $this->getContentSignature((string)$content);
261  }
262  $allowedInvocationIndex = $this->searchAllowedInvocation(
263  $command,
264  $combinedFileIdentifier,
265  $contentSignature
266  );
267 
268  if ($allowedInvocationIndex === null) {
270  sprintf(
271  'Persisting form definition "%s" is denied',
272  $combinedFileIdentifier
273  ),
274  1530281202
275  );
276  }
277  unset($this->allowedInvocations[$allowedInvocationIndex]);
278  }
279 
286  protected function searchAllowedInvocation(
287  string $command,
288  string $combinedFileIdentifier,
289  string $contentSignature = null
290  ) {
291  foreach ($this->allowedInvocations as $index => $allowedInvocation) {
292  if (
293  $command === $allowedInvocation['command']
294  && $combinedFileIdentifier === $allowedInvocation['combinedFileIdentifier']
295  && $contentSignature === $allowedInvocation['contentSignature']
296  ) {
297  return $index;
298  }
299  }
300  return null;
301  }
302 
308  protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
309  {
310  return sprintf(
311  '%d:%s%s',
312  $folder->getStorage()->getUid(),
313  $folder->getIdentifier(),
314  $fileName
315  );
316  }
317 
322  protected function isFormDefinition(string $identifier): bool
323  {
325  $identifier,
327  );
328  }
329 
334  protected function isRecycleFolder(FolderInterface $folder): bool
335  {
336  $role = $folder->getStorage()->getRole($folder);
337  return $role === FolderInterface::ROLE_RECYCLER;
338  }
339 }
onPreFileSetContents(FileInterface $file, $content=null)
defineInvocation(string $command, bool $type=null)
assertFileName(string $command, string $combinedFileIdentifier, string $content=null)
searchAllowedInvocation(string $command, string $combinedFileIdentifier, string $contentSignature=null)
onPreFileRename(FileInterface $file, string $targetFileName)
static hmac($input, $additionalSecret='')
onPreFileMove(FileInterface $file, FolderInterface $targetFolder, string $targetFileName)
allowInvocation(string $command, string $combinedFileIdentifier, string $contentSignature=null)
buildCombinedIdentifier(FolderInterface $folder, string $fileName)
onPreFileReplace(FileInterface $file, string $localFilePath)
onPreFileCreate(string $fileName, FolderInterface $targetFolder)
onPreFileAdd(string $targetFileName, FolderInterface $targetFolder, string $sourceFilePath)
static endsWith($haystack, $needle)