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