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