[TASK] Removes extra empty lines
[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 {
114 $this->originalFile = $originalFile;
115 $this->originalFileSha1 = $this->originalFile->getSha1();
116 $this->storage = $originalFile->getStorage()->getProcessingFolder()->getStorage();
117 $this->taskType = $taskType;
118 $this->processingConfiguration = $processingConfiguration;
119 if (is_array($databaseRow)) {
120 $this->reconstituteFromDatabaseRecord($databaseRow);
121 }
122 $this->taskTypeRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry::class);
123 }
124
125 /**
126 * Creates a ProcessedFile object from a database record.
127 *
128 * @param array $databaseRow
129 * @return ProcessedFile
130 */
131 protected function reconstituteFromDatabaseRecord(array $databaseRow)
132 {
133 $this->taskType = $this->taskType ?: $databaseRow['task_type'];
134 $this->processingConfiguration = $this->processingConfiguration ?: unserialize($databaseRow['configuration']);
135
136 $this->originalFileSha1 = $databaseRow['originalfilesha1'];
137 $this->identifier = $databaseRow['identifier'];
138 $this->name = $databaseRow['name'];
139 $this->properties = $databaseRow;
140 }
141
142 /********************************
143 * VARIOUS FILE PROPERTY GETTERS
144 ********************************/
145
146 /**
147 * Returns a unique checksum for this file's processing configuration and original file.
148 *
149 * @return string
150 */
151 // @todo replace these usages with direct calls to the task object
152 public function calculateChecksum()
153 {
154 return $this->getTask()->getConfigurationChecksum();
155 }
156
157 /*******************
158 * CONTENTS RELATED
159 *******************/
160 /**
161 * Replace the current file contents with the given string
162 *
163 * @param string $contents The contents to write to the file.
164 * @return File The file object (allows chaining).
165 * @throws \BadMethodCallException
166 */
167 public function setContents($contents)
168 {
169 throw new \BadMethodCallException('Setting contents not possible for processed file.', 1305438528);
170 }
171
172 /**
173 * Injects a local file, which is a processing result into the object.
174 *
175 * @param string $filePath
176 * @return void
177 * @throws \RuntimeException
178 */
179 public function updateWithLocalFile($filePath)
180 {
181 if ($this->identifier === null) {
182 throw new \RuntimeException('Cannot update original file!', 1350582054);
183 }
184 $processingFolder = $this->originalFile->getStorage()->getProcessingFolder();
185 $addedFile = $this->storage->updateProcessedFile($filePath, $this, $processingFolder);
186
187 // Update some related properties
188 $this->identifier = $addedFile->getIdentifier();
189 $this->originalFileSha1 = $this->originalFile->getSha1();
190 $this->updateProperties($addedFile->getProperties());
191 $this->deleted = false;
192 $this->updated = true;
193 }
194
195 /*****************************************
196 * STORAGE AND MANAGEMENT RELATED METHDOS
197 *****************************************/
198 /**
199 * Returns TRUE if this file is indexed
200 *
201 * @return bool
202 */
203 public function isIndexed()
204 {
205 // Processed files are never indexed; instead you might be looking for isPersisted()
206 return false;
207 }
208
209 /**
210 * Checks whether the ProcessedFile already has an entry in sys_file_processedfile table
211 *
212 * @return bool
213 */
214 public function isPersisted()
215 {
216 return is_array($this->properties) && array_key_exists('uid', $this->properties) && $this->properties['uid'] > 0;
217 }
218
219 /**
220 * Checks whether the ProcessedFile Object is newly created
221 *
222 * @return bool
223 */
224 public function isNew()
225 {
226 return !$this->isPersisted();
227 }
228
229 /**
230 * Checks whether the object since last reconstitution, and therefore
231 * needs persistence again
232 *
233 * @return bool
234 */
235 public function isUpdated()
236 {
237 return $this->updated;
238 }
239
240 /**
241 * Sets a new file name
242 *
243 * @param string $name
244 */
245 public function setName($name)
246 {
247 // Remove the existing file
248 if ($this->name !== $name && $this->name !== '' && $this->exists()) {
249 $this->delete();
250 }
251
252 $this->name = $name;
253 // @todo this is a *weird* hack that will fail if the storage is non-hierarchical!
254 $this->identifier = $this->storage->getProcessingFolder()->getIdentifier() . $this->name;
255
256 $this->updated = true;
257 }
258
259 /******************
260 * SPECIAL METHODS
261 ******************/
262
263 /**
264 * Returns TRUE if this file is already processed.
265 *
266 * @return bool
267 */
268 public function isProcessed()
269 {
270 return $this->updated || ($this->isPersisted() && !$this->needsReprocessing());
271 }
272
273 /**
274 * Getter for the Original, unprocessed File
275 *
276 * @return File
277 */
278 public function getOriginalFile()
279 {
280 return $this->originalFile;
281 }
282
283 /**
284 * Get the identifier of the file
285 *
286 * If there is no processed file in the file system (as the original file did not have to be modified e.g.
287 * when the original image is in the boundaries of the maxW/maxH stuff), then just return the identifier of
288 * the original file
289 *
290 * @return string
291 */
292 public function getIdentifier()
293 {
294 return (!$this->usesOriginalFile()) ? $this->identifier : $this->getOriginalFile()->getIdentifier();
295 }
296
297 /**
298 * Get the name of the file
299 *
300 * If there is no processed file in the file system (as the original file did not have to be modified e.g.
301 * when the original image is in the boundaries of the maxW/maxH stuff)
302 * then just return the name of the original file
303 *
304 * @return string
305 */
306 public function getName()
307 {
308 if ($this->usesOriginalFile()) {
309 return $this->originalFile->getName();
310 } else {
311 return $this->name;
312 }
313 }
314
315 /**
316 * Updates properties of this object. Do not use this to reconstitute an object from the database; use
317 * reconstituteFromDatabaseRecord() instead!
318 *
319 * @param array $properties
320 */
321 public function updateProperties(array $properties)
322 {
323 if (!is_array($this->properties)) {
324 $this->properties = array();
325 }
326
327 if (array_key_exists('uid', $properties) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['uid'])) {
328 $this->properties['uid'] = $properties['uid'];
329 }
330
331 // @todo we should have a blacklist of properties that might not be updated
332 $this->properties = array_merge($this->properties, $properties);
333
334 // @todo when should this update be done?
335 if (!$this->isUnchanged() && $this->exists()) {
336 $this->properties = array_merge($this->properties, $this->storage->getFileInfo($this));
337 }
338 }
339
340 /**
341 * Basic array function for the DB update
342 *
343 * @return array
344 */
345 public function toArray()
346 {
347 if ($this->usesOriginalFile()) {
348 $properties = $this->originalFile->getProperties();
349 unset($properties['uid']);
350 unset($properties['pid']);
351 unset($properties['identifier']);
352 unset($properties['name']);
353
354 // Use width + height set in processed file
355 $properties['width'] = $this->properties['width'];
356 $properties['height'] = $this->properties['height'];
357 } else {
358 $properties = $this->properties;
359 $properties['identifier'] = $this->getIdentifier();
360 $properties['name'] = $this->getName();
361 }
362
363 $properties['configuration'] = serialize($this->processingConfiguration);
364
365 return array_merge($properties, array(
366 'storage' => $this->getStorage()->getUid(),
367 'checksum' => $this->calculateChecksum(),
368 'task_type' => $this->taskType,
369 'configurationsha1' => sha1($properties['configuration']),
370 'original' => $this->originalFile->getUid(),
371 'originalfilesha1' => $this->originalFileSha1
372 ));
373 }
374
375 /**
376 * Returns TRUE if this file has not been changed during processing (i.e., we just deliver the original file)
377 *
378 * @return bool
379 */
380 protected function isUnchanged()
381 {
382 return !$this->properties['width'] && $this->usesOriginalFile();
383 }
384
385 /**
386 * @return void
387 */
388 public function setUsesOriginalFile()
389 {
390 // @todo check if some of these properties can/should be set in a generic update method
391 $this->identifier = $this->originalFile->getIdentifier();
392 $this->updated = true;
393 $this->originalFileSha1 = $this->originalFile->getSha1();
394 }
395
396 /**
397 * @return bool
398 */
399 public function usesOriginalFile()
400 {
401 return $this->identifier == null || $this->identifier === $this->originalFile->getIdentifier();
402 }
403
404 /**
405 * Returns TRUE if the original file of this file changed and the file should be processed again.
406 *
407 * @return bool
408 */
409 public function isOutdated()
410 {
411 return $this->needsReprocessing();
412 }
413
414 /**
415 * Delete processed file
416 *
417 * @param bool $force
418 * @return bool
419 */
420 public function delete($force = false)
421 {
422 if (!$force && $this->isUnchanged()) {
423 return false;
424 }
425 // Only delete file when original isn't used
426 if (!$this->usesOriginalFile()) {
427 return parent::delete();
428 } else {
429 return true;
430 }
431 }
432
433 /**
434 * Getter for file-properties
435 *
436 * @param string $key
437 *
438 * @return mixed
439 */
440 public function getProperty($key)
441 {
442 // The uid always (!) has to come from this file and never the original file (see getOriginalFile() to get this)
443 if ($this->isUnchanged() && $key !== 'uid') {
444 return $this->originalFile->getProperty($key);
445 } else {
446 return $this->properties[$key];
447 }
448 }
449
450 /**
451 * Returns the uid of this file
452 *
453 * @return int
454 */
455 public function getUid()
456 {
457 return $this->properties['uid'];
458 }
459
460 /**
461 * Checks if the ProcessedFile needs reprocessing
462 *
463 * @return bool
464 */
465 public function needsReprocessing()
466 {
467 $fileMustBeRecreated = false;
468
469 // if original is missing we can not reprocess the file
470 if ($this->originalFile->isMissing()) {
471 return false;
472 }
473
474 // processedFile does not exist
475 if (!$this->usesOriginalFile() && !$this->exists()) {
476 $fileMustBeRecreated = true;
477 }
478
479 // hash does not match
480 if (array_key_exists('checksum', $this->properties) && $this->calculateChecksum() !== $this->properties['checksum']) {
481 $fileMustBeRecreated = true;
482 }
483
484 // original file changed
485 if ($this->originalFile->getSha1() !== $this->originalFileSha1) {
486 $fileMustBeRecreated = true;
487 }
488
489 if (!array_key_exists('uid', $this->properties)) {
490 $fileMustBeRecreated = true;
491 }
492
493 // remove outdated file
494 if ($fileMustBeRecreated && $this->exists()) {
495 $this->delete();
496 }
497 return $fileMustBeRecreated;
498 }
499
500 /**
501 * Returns the processing information
502 *
503 * @return array
504 */
505 public function getProcessingConfiguration()
506 {
507 return $this->processingConfiguration;
508 }
509
510 /**
511 * Getter for the task identifier.
512 *
513 * @return string
514 */
515 public function getTaskIdentifier()
516 {
517 return $this->taskType;
518 }
519
520 /**
521 * Returns the task object associated with this processed file.
522 *
523 * @return Processing\TaskInterface
524 * @throws \RuntimeException
525 */
526 public function getTask()
527 {
528 if ($this->task == null) {
529 $this->task = $this->taskTypeRegistry->getTaskForType($this->taskType, $this, $this->processingConfiguration);
530 }
531
532 return $this->task;
533 }
534
535 /**
536 * Generate the name of of the new File
537 *
538 * @return string
539 */
540 public function generateProcessedFileNameWithoutExtension()
541 {
542 $name = $this->originalFile->getNameWithoutExtension();
543 $name .= '_' . $this->originalFile->getUid();
544 $name .= '_' . $this->calculateChecksum();
545
546 return $name;
547 }
548
549 /**
550 * Returns a publicly accessible URL for this file
551 *
552 * @param bool $relativeToCurrentScript Determines whether the URL returned should be relative to the current script, in case it is relative at all
553 * @return NULL|string NULL if file is deleted, the generated URL otherwise
554 */
555 public function getPublicUrl($relativeToCurrentScript = false)
556 {
557 if ($this->deleted) {
558 return null;
559 } elseif ($this->usesOriginalFile()) {
560 return $this->getOriginalFile()->getPublicUrl($relativeToCurrentScript);
561 } else {
562 return $this->getStorage()->getPublicUrl($this, $relativeToCurrentScript);
563 }
564 }
565 }