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