73d1e8f825bf8ceec46d2dda6ed9bd0ca0f32fc9
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / ProcessedFile.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Core\Utility\MathUtility;
19
20 /**
21 * Representation of a specific processed version of a file. These are created by the FileProcessingService,
22 * which in turn uses helper classes for doing the actual file processing. See there for a detailed description.
23 *
24 * Objects of this class may be freshly created during runtime or being fetched from the database. The latter
25 * indicates that the file has been processed earlier and was then cached.
26 *
27 * Each processed file—besides belonging to one file—has been created for a certain task (context) and
28 * configuration. All these won't change during the lifetime of a processed file; the only thing
29 * that can change is the original file, or rather it's contents. In that case, the processed file has to
30 * be processed again. Detecting this is done via comparing the current SHA1 hash of the original file against
31 * the one it had at the time the file was processed.
32 * The configuration of a processed file indicates what should be done to the original file to create the
33 * processed version. This may include things like cropping, scaling, rotating, flipping or using some special
34 * magic.
35 * A file may also meet the expectations set in the configuration without any processing. In that case, the
36 * ProcessedFile object still exists, but there is no physical file directly linked to it. Instead, it then
37 * redirects most method calls to the original file object. The data of these objects are also stored in the
38 * database, to indicate that no processing is required. With such files, the identifier and name fields in the
39 * database are empty to show this.
40 */
41 class ProcessedFile extends AbstractFile
42 {
43 /*********************************************
44 * FILE PROCESSING CONTEXTS
45 *********************************************/
46 /**
47 * Basic processing context to get a processed image with smaller
48 * width/height to render a preview
49 */
50 const CONTEXT_IMAGEPREVIEW = 'Image.Preview';
51 /**
52 * Standard processing context for the frontend, that was previously
53 * in \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getImgResource which only takes cropping, masking and scaling
54 * into account
55 */
56 const CONTEXT_IMAGECROPSCALEMASK = 'Image.CropScaleMask';
57
58 /**
59 * Processing context, i.e. the type of processing done
60 *
61 * @var string
62 */
63 protected $taskType;
64
65 /**
66 * @var Processing\TaskInterface
67 */
68 protected $task;
69
70 /**
71 * @var Processing\TaskTypeRegistry
72 */
73 protected $taskTypeRegistry;
74
75 /**
76 * Processing configuration
77 *
78 * @var array
79 */
80 protected $processingConfiguration;
81
82 /**
83 * Reference to the original file this processed file has been created from.
84 *
85 * @var File
86 */
87 protected $originalFile;
88
89 /**
90 * The SHA1 hash of the original file this processed version has been created for.
91 * Is used for detecting changes if the original file has been changed and thus
92 * we have to recreate this processed file.
93 *
94 * @var string
95 */
96 protected $originalFileSha1;
97
98 /**
99 * A flag that shows if this object has been updated during its lifetime, i.e. the file has been
100 * replaced with a new one.
101 *
102 * @var bool
103 */
104 protected $updated = false;
105
106 /**
107 * Constructor for a processed file object. Should normally not be used
108 * directly, use the corresponding factory methods instead.
109 *
110 * @param File $originalFile
111 * @param string $taskType
112 * @param array $processingConfiguration
113 * @param array $databaseRow
114 */
115 public function __construct(File $originalFile, $taskType, array $processingConfiguration, array $databaseRow = null)
116 {
117 $this->originalFile = $originalFile;
118 $this->originalFileSha1 = $this->originalFile->getSha1();
119 $this->storage = $originalFile->getStorage()->getProcessingFolder()->getStorage();
120 $this->taskType = $taskType;
121 if ($taskType === self::CONTEXT_IMAGEPREVIEW) {
122 $processingConfiguration = array_merge(['width' => 64, 'height' => 64], $processingConfiguration);
123 $processingConfiguration['width'] = MathUtility::forceIntegerInRange($processingConfiguration['width'], 1, 1000);
124 $processingConfiguration['height'] = MathUtility::forceIntegerInRange($processingConfiguration['height'], 1, 1000);
125 }
126 $this->processingConfiguration = $processingConfiguration;
127 if (is_array($databaseRow)) {
128 $this->reconstituteFromDatabaseRecord($databaseRow);
129 }
130 $this->taskTypeRegistry = GeneralUtility::makeInstance(Processing\TaskTypeRegistry::class);
131 }
132
133 /**
134 * Creates a ProcessedFile object from a database record.
135 *
136 * @param array $databaseRow
137 * @return ProcessedFile
138 */
139 protected function reconstituteFromDatabaseRecord(array $databaseRow)
140 {
141 $this->taskType = $this->taskType ?: $databaseRow['task_type'];
142 $this->processingConfiguration = $this->processingConfiguration ?: unserialize($databaseRow['configuration']);
143
144 $this->originalFileSha1 = $databaseRow['originalfilesha1'];
145 $this->identifier = $databaseRow['identifier'];
146 $this->name = $databaseRow['name'];
147 $this->properties = $databaseRow;
148 }
149
150 /********************************
151 * VARIOUS FILE PROPERTY GETTERS
152 ********************************/
153
154 /**
155 * Returns a unique checksum for this file's processing configuration and original file.
156 *
157 * @return string
158 */
159 // @todo replace these usages with direct calls to the task object
160 public function calculateChecksum()
161 {
162 return $this->getTask()->getConfigurationChecksum();
163 }
164
165 /*******************
166 * CONTENTS RELATED
167 *******************/
168 /**
169 * Replace the current file contents with the given string
170 *
171 * @param string $contents The contents to write to the file.
172 * @return File The file object (allows chaining).
173 * @throws \BadMethodCallException
174 */
175 public function setContents($contents)
176 {
177 throw new \BadMethodCallException('Setting contents not possible for processed file.', 1305438528);
178 }
179
180 /**
181 * Injects a local file, which is a processing result into the object.
182 *
183 * @param string $filePath
184 * @throws \RuntimeException
185 */
186 public function updateWithLocalFile($filePath)
187 {
188 if ($this->identifier === null) {
189 throw new \RuntimeException('Cannot update original file!', 1350582054);
190 }
191 $processingFolder = $this->originalFile->getStorage()->getProcessingFolder($this->originalFile);
192 $addedFile = $this->storage->updateProcessedFile($filePath, $this, $processingFolder);
193
194 // Update some related properties
195 $this->identifier = $addedFile->getIdentifier();
196 $this->originalFileSha1 = $this->originalFile->getSha1();
197 if ($addedFile instanceof AbstractFile) {
198 $this->updateProperties($addedFile->getProperties());
199 }
200 $this->deleted = false;
201 $this->updated = true;
202 }
203
204 /*****************************************
205 * STORAGE AND MANAGEMENT RELATED METHDOS
206 *****************************************/
207 /**
208 * Returns TRUE if this file is indexed
209 *
210 * @return bool
211 */
212 public function isIndexed()
213 {
214 // Processed files are never indexed; instead you might be looking for isPersisted()
215 return false;
216 }
217
218 /**
219 * Checks whether the ProcessedFile already has an entry in sys_file_processedfile table
220 *
221 * @return bool
222 */
223 public function isPersisted()
224 {
225 return is_array($this->properties) && array_key_exists('uid', $this->properties) && $this->properties['uid'] > 0;
226 }
227
228 /**
229 * Checks whether the ProcessedFile Object is newly created
230 *
231 * @return bool
232 */
233 public function isNew()
234 {
235 return !$this->isPersisted();
236 }
237
238 /**
239 * Checks whether the object since last reconstitution, and therefore
240 * needs persistence again
241 *
242 * @return bool
243 */
244 public function isUpdated()
245 {
246 return $this->updated;
247 }
248
249 /**
250 * Sets a new file name
251 *
252 * @param string $name
253 */
254 public function setName($name)
255 {
256 // Remove the existing file
257 if ($this->name !== $name && $this->name !== '' && $this->exists()) {
258 $this->delete();
259 }
260
261 $this->name = $name;
262 // @todo this is a *weird* hack that will fail if the storage is non-hierarchical!
263 $this->identifier = $this->storage->getProcessingFolder($this->originalFile)->getIdentifier() . $this->name;
264
265 $this->updated = true;
266 }
267
268 /******************
269 * SPECIAL METHODS
270 ******************/
271
272 /**
273 * Returns TRUE if this file is already processed.
274 *
275 * @return bool
276 */
277 public function isProcessed()
278 {
279 return $this->updated || ($this->isPersisted() && !$this->needsReprocessing());
280 }
281
282 /**
283 * Getter for the Original, unprocessed File
284 *
285 * @return File
286 */
287 public function getOriginalFile()
288 {
289 return $this->originalFile;
290 }
291
292 /**
293 * Get the identifier of the file
294 *
295 * If there is no processed file in the file system (as the original file did not have to be modified e.g.
296 * when the original image is in the boundaries of the maxW/maxH stuff), then just return the identifier of
297 * the original file
298 *
299 * @return string
300 */
301 public function getIdentifier()
302 {
303 return (!$this->usesOriginalFile()) ? $this->identifier : $this->getOriginalFile()->getIdentifier();
304 }
305
306 /**
307 * Get the name of the file
308 *
309 * If there is no processed file in the file system (as the original file did not have to be modified e.g.
310 * when the original image is in the boundaries of the maxW/maxH stuff)
311 * then just return the name of the original file
312 *
313 * @return string
314 */
315 public function getName()
316 {
317 if ($this->usesOriginalFile()) {
318 return $this->originalFile->getName();
319 } else {
320 return $this->name;
321 }
322 }
323
324 /**
325 * Updates properties of this object. Do not use this to reconstitute an object from the database; use
326 * reconstituteFromDatabaseRecord() instead!
327 *
328 * @param array $properties
329 */
330 public function updateProperties(array $properties)
331 {
332 if (!is_array($this->properties)) {
333 $this->properties = [];
334 }
335
336 if (array_key_exists('uid', $properties) && MathUtility::canBeInterpretedAsInteger($properties['uid'])) {
337 $this->properties['uid'] = $properties['uid'];
338 }
339
340 // @todo we should have a blacklist of properties that might not be updated
341 $this->properties = array_merge($this->properties, $properties);
342
343 // @todo when should this update be done?
344 if (!$this->isUnchanged() && $this->exists()) {
345 $this->properties = array_merge($this->properties, $this->storage->getFileInfo($this));
346 }
347 }
348
349 /**
350 * Basic array function for the DB update
351 *
352 * @return array
353 */
354 public function toArray()
355 {
356 if ($this->usesOriginalFile()) {
357 $properties = $this->originalFile->getProperties();
358 unset($properties['uid']);
359 unset($properties['pid']);
360 unset($properties['identifier']);
361 unset($properties['name']);
362
363 // Use width + height set in processed file
364 $properties['width'] = $this->properties['width'];
365 $properties['height'] = $this->properties['height'];
366 } else {
367 $properties = $this->properties;
368 $properties['identifier'] = $this->getIdentifier();
369 $properties['name'] = $this->getName();
370 }
371
372 $properties['configuration'] = serialize($this->processingConfiguration);
373
374 return array_merge($properties, [
375 'storage' => $this->getStorage()->getUid(),
376 'checksum' => $this->calculateChecksum(),
377 'task_type' => $this->taskType,
378 'configurationsha1' => sha1($properties['configuration']),
379 'original' => $this->originalFile->getUid(),
380 'originalfilesha1' => $this->originalFileSha1
381 ]);
382 }
383
384 /**
385 * Returns TRUE if this file has not been changed during processing (i.e., we just deliver the original file)
386 *
387 * @return bool
388 */
389 protected function isUnchanged()
390 {
391 return !$this->properties['width'] && $this->usesOriginalFile();
392 }
393
394 /**
395 * Defines that the original file should be used.
396 */
397 public function setUsesOriginalFile()
398 {
399 // @todo check if some of these properties can/should be set in a generic update method
400 $this->identifier = $this->originalFile->getIdentifier();
401 $this->updated = true;
402 $this->originalFileSha1 = $this->originalFile->getSha1();
403 }
404
405 /**
406 * @return bool
407 */
408 public function usesOriginalFile()
409 {
410 return $this->identifier == null || $this->identifier === $this->originalFile->getIdentifier();
411 }
412
413 /**
414 * Returns TRUE if the original file of this file changed and the file should be processed again.
415 *
416 * @return bool
417 */
418 public function isOutdated()
419 {
420 return $this->needsReprocessing();
421 }
422
423 /**
424 * Delete processed file
425 *
426 * @param bool $force
427 * @return bool
428 */
429 public function delete($force = false)
430 {
431 if (!$force && $this->isUnchanged()) {
432 return false;
433 }
434 // Only delete file when original isn't used
435 if (!$this->usesOriginalFile()) {
436 return parent::delete();
437 }
438 return true;
439 }
440
441 /**
442 * Getter for file-properties
443 *
444 * @param string $key
445 *
446 * @return mixed
447 */
448 public function getProperty($key)
449 {
450 // The uid always (!) has to come from this file and never the original file (see getOriginalFile() to get this)
451 if ($this->isUnchanged() && $key !== 'uid') {
452 return $this->originalFile->getProperty($key);
453 } else {
454 return $this->properties[$key];
455 }
456 }
457
458 /**
459 * Returns the uid of this file
460 *
461 * @return int
462 */
463 public function getUid()
464 {
465 return $this->properties['uid'];
466 }
467
468 /**
469 * Checks if the ProcessedFile needs reprocessing
470 *
471 * @return bool
472 */
473 public function needsReprocessing()
474 {
475 $fileMustBeRecreated = false;
476
477 // if original is missing we can not reprocess the file
478 if ($this->originalFile->isMissing()) {
479 return false;
480 }
481
482 // processedFile does not exist
483 if (!$this->usesOriginalFile() && !$this->exists()) {
484 $fileMustBeRecreated = true;
485 }
486
487 // hash does not match
488 if (array_key_exists('checksum', $this->properties) && $this->calculateChecksum() !== $this->properties['checksum']) {
489 $fileMustBeRecreated = true;
490 }
491
492 // original file changed
493 if ($this->originalFile->getSha1() !== $this->originalFileSha1) {
494 $fileMustBeRecreated = true;
495 }
496
497 if (!array_key_exists('uid', $this->properties)) {
498 $fileMustBeRecreated = true;
499 }
500
501 // remove outdated file
502 if ($fileMustBeRecreated && $this->exists()) {
503 $this->delete();
504 }
505 return $fileMustBeRecreated;
506 }
507
508 /**
509 * Returns the processing information
510 *
511 * @return array
512 */
513 public function getProcessingConfiguration()
514 {
515 return $this->processingConfiguration;
516 }
517
518 /**
519 * Getter for the task identifier.
520 *
521 * @return string
522 */
523 public function getTaskIdentifier()
524 {
525 return $this->taskType;
526 }
527
528 /**
529 * Returns the task object associated with this processed file.
530 *
531 * @return Processing\TaskInterface
532 * @throws \RuntimeException
533 */
534 public function getTask()
535 {
536 if ($this->task == null) {
537 $this->task = $this->taskTypeRegistry->getTaskForType($this->taskType, $this, $this->processingConfiguration);
538 }
539
540 return $this->task;
541 }
542
543 /**
544 * Generate the name of of the new File
545 *
546 * @return string
547 */
548 public function generateProcessedFileNameWithoutExtension()
549 {
550 $name = $this->originalFile->getNameWithoutExtension();
551 $name .= '_' . $this->originalFile->getUid();
552 $name .= '_' . $this->calculateChecksum();
553
554 return $name;
555 }
556
557 /**
558 * Returns a publicly accessible URL for this file
559 *
560 * @param bool $relativeToCurrentScript Determines whether the URL returned should be relative to the current script, in case it is relative at all
561 * @return NULL|string NULL if file is deleted, the generated URL otherwise
562 */
563 public function getPublicUrl($relativeToCurrentScript = false)
564 {
565 if ($this->deleted) {
566 return null;
567 } elseif ($this->usesOriginalFile()) {
568 return $this->getOriginalFile()->getPublicUrl($relativeToCurrentScript);
569 } else {
570 return $this->getStorage()->getPublicUrl($this, $relativeToCurrentScript);
571 }
572 }
573 }