Passed
Push — master ( 811217...98b68e )
by
unknown
12:07
created

ProcessedFile::getPublicUrl()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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