1<?php
2namespace 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
17use TYPO3\CMS\Core\Utility\MathUtility;
18use TYPO3\CMS\Core\Utility\PathUtility;
19
20/**
21 * Abstract file representation in the file abstraction layer.
22 */
23abstract class AbstractFile implements FileInterface
24{
25    /**
26     * Various file properties
27     *
28     * Note that all properties, which only the persisted (indexed) files have are stored in this
29     * overall properties array only. The only properties which really exist as object properties of
30     * the file object are the storage, the identifier, the fileName and the indexing status.
31     *
32     * @var array
33     */
34    protected $properties;
35
36    /**
37     * The storage this file is located in
38     *
39     * @var ResourceStorage
40     */
41    protected $storage;
42
43    /**
44     * The identifier of this file to identify it on the storage.
45     * On some drivers, this is the path to the file, but drivers could also just
46     * provide any other unique identifier for this file on the specific storage.
47     *
48     * @var string
49     */
50    protected $identifier;
51
52    /**
53     * The file name of this file
54     *
55     * @var string
56     */
57    protected $name;
58
59    /**
60     * If set to true, this file is regarded as being deleted.
61     *
62     * @var bool
63     */
64    protected $deleted = false;
65
66    /**
67     * any other file
68     */
69    const FILETYPE_UNKNOWN = 0;
70
71    /**
72     * Any kind of text
73     * @see http://www.iana.org/assignments/media-types/text
74     */
75    const FILETYPE_TEXT = 1;
76
77    /**
78     * Any kind of image
79     * @see http://www.iana.org/assignments/media-types/image
80     */
81    const FILETYPE_IMAGE = 2;
82
83    /**
84     * Any kind of audio file
85     * @see http://www.iana.org/assignments/media-types/audio
86     */
87    const FILETYPE_AUDIO = 3;
88
89    /**
90     * Any kind of video
91     * @see http://www.iana.org/assignments/media-types/video
92     */
93    const FILETYPE_VIDEO = 4;
94
95    /**
96     * Any kind of application
97     * @see http://www.iana.org/assignments/media-types/application
98     */
99    const FILETYPE_APPLICATION = 5;
100
101    /******************
102     * VARIOUS FILE PROPERTY GETTERS
103     ******************/
104    /**
105     * Returns true if the given property key exists for this file.
106     *
107     * @param string $key
108     * @return bool
109     */
110    public function hasProperty($key)
111    {
112        return array_key_exists($key, $this->properties);
113    }
114
115    /**
116     * Returns a property value
117     *
118     * @param string $key
119     * @return mixed Property value
120     */
121    public function getProperty($key)
122    {
123        if ($this->hasProperty($key)) {
124            return $this->properties[$key];
125        }
126        return null;
127    }
128
129    /**
130     * Returns the properties of this object.
131     *
132     * @return array
133     */
134    public function getProperties()
135    {
136        return $this->properties;
137    }
138
139    /**
140     * Returns the identifier of this file
141     *
142     * @return string
143     */
144    public function getIdentifier()
145    {
146        return $this->identifier;
147    }
148
149    /**
150     * Get hashed identifier
151     *
152     * @return string
153     */
154    public function getHashedIdentifier()
155    {
156        return $this->properties['identifier_hash'];
157    }
158
159    /**
160     * Returns the name of this file
161     *
162     * @return string
163     */
164    public function getName()
165    {
166        // Do not check if file has been deleted because we might need the
167        // name for undeleting it.
168        return $this->name;
169    }
170
171    /**
172     * Returns the basename (the name without extension) of this file.
173     *
174     * @return string
175     */
176    public function getNameWithoutExtension()
177    {
178        return PathUtility::pathinfo($this->getName(), PATHINFO_FILENAME);
179    }
180
181    /**
182     * Returns the size of this file
183     *
184     * @throws \RuntimeException
185     * @return int|null Returns null if size is not available for the file
186     */
187    public function getSize()
188    {
189        if ($this->deleted) {
190            throw new \RuntimeException('File has been deleted.', 1329821480);
191        }
192        if (empty($this->properties['size'])) {
193            $size = array_pop($this->getStorage()->getFileInfoByIdentifier($this->getIdentifier(), ['size']));
194        } else {
195            $size = $this->properties['size'];
196        }
197        return $size ? (int)$size : null;
198    }
199
200    /**
201     * Returns the uid of this file
202     *
203     * @return int
204     */
205    public function getUid()
206    {
207        return (int)$this->getProperty('uid');
208    }
209
210    /**
211     * Returns the Sha1 of this file
212     *
213     * @throws \RuntimeException
214     * @return string
215     */
216    public function getSha1()
217    {
218        if ($this->deleted) {
219            throw new \RuntimeException('File has been deleted.', 1329821481);
220        }
221        return $this->getStorage()->hashFile($this, 'sha1');
222    }
223
224    /**
225     * Returns the creation time of the file as Unix timestamp
226     *
227     * @throws \RuntimeException
228     * @return int
229     */
230    public function getCreationTime()
231    {
232        if ($this->deleted) {
233            throw new \RuntimeException('File has been deleted.', 1329821487);
234        }
235        return (int)$this->getProperty('creation_date');
236    }
237
238    /**
239     * Returns the date (as UNIX timestamp) the file was last modified.
240     *
241     * @throws \RuntimeException
242     * @return int
243     */
244    public function getModificationTime()
245    {
246        if ($this->deleted) {
247            throw new \RuntimeException('File has been deleted.', 1329821488);
248        }
249        return (int)$this->getProperty('modification_date');
250    }
251
252    /**
253     * Get the extension of this file in a lower-case variant
254     *
255     * @return string The file extension
256     */
257    public function getExtension()
258    {
259        $pathinfo = PathUtility::pathinfo($this->getName());
260
261        $extension = strtolower($pathinfo['extension'] ?? '');
262
263        return $extension;
264    }
265
266    /**
267     * Get the MIME type of this file
268     *
269     * @return string mime type
270     */
271    public function getMimeType()
272    {
273        return $this->properties['mime_type'] ?: array_pop($this->getStorage()->getFileInfoByIdentifier($this->getIdentifier(), ['mimetype']));
274    }
275
276    /**
277     * Returns the fileType of this file
278     * basically there are only five main "file types"
279     * "audio"
280     * "image"
281     * "software"
282     * "text"
283     * "video"
284     * "other"
285     * see the constants in this class
286     *
287     * @return int $fileType
288     */
289    public function getType()
290    {
291        // this basically extracts the mimetype and guess the filetype based
292        // on the first part of the mimetype works for 99% of all cases, and
293        // we don't need to make an SQL statement like EXT:media does currently
294        if (!$this->properties['type']) {
295            $mimeType = $this->getMimeType();
296            list($fileType) = explode('/', $mimeType);
297            switch (strtolower($fileType)) {
298                case 'text':
299                    $this->properties['type'] = self::FILETYPE_TEXT;
300                    break;
301                case 'image':
302                    $this->properties['type'] = self::FILETYPE_IMAGE;
303                    break;
304                case 'audio':
305                    $this->properties['type'] = self::FILETYPE_AUDIO;
306                    break;
307                case 'video':
308                    $this->properties['type'] = self::FILETYPE_VIDEO;
309                    break;
310                case 'application':
311
312                case 'software':
313                    $this->properties['type'] = self::FILETYPE_APPLICATION;
314                    break;
315                default:
316                    $this->properties['type'] = self::FILETYPE_UNKNOWN;
317            }
318        }
319        return (int)$this->properties['type'];
320    }
321
322    /******************
323     * CONTENTS RELATED
324     ******************/
325    /**
326     * Get the contents of this file
327     *
328     * @throws \RuntimeException
329     * @return string File contents
330     */
331    public function getContents()
332    {
333        if ($this->deleted) {
334            throw new \RuntimeException('File has been deleted.', 1329821479);
335        }
336        return $this->getStorage()->getFileContents($this);
337    }
338
339    /**
340     * Replace the current file contents with the given string
341     *
342     * @param string $contents The contents to write to the file.
343     *
344     * @throws \RuntimeException
345     * @return File The file object (allows chaining).
346     */
347    public function setContents($contents)
348    {
349        if ($this->deleted) {
350            throw new \RuntimeException('File has been deleted.', 1329821478);
351        }
352        $this->getStorage()->setFileContents($this, $contents);
353        return $this;
354    }
355
356    /****************************************
357     * STORAGE AND MANAGEMENT RELATED METHDOS
358     ****************************************/
359
360    /**
361     * Get the storage this file is located in
362     *
363     * @return ResourceStorage
364     * @throws \RuntimeException
365     */
366    public function getStorage()
367    {
368        if ($this->storage === null) {
369            throw new \RuntimeException('You\'re using fileObjects without a storage.', 1381570091);
370        }
371        return $this->storage;
372    }
373
374    /**
375     * Checks if this file exists. This should normally always return TRUE;
376     * it might only return FALSE when this object has been created from an
377     * index record without checking for.
378     *
379     * @return bool TRUE if this file physically exists
380     */
381    public function exists()
382    {
383        if ($this->deleted) {
384            return false;
385        }
386        return $this->storage->hasFile($this->getIdentifier());
387    }
388
389    /**
390     * Sets the storage this file is located in. This is only meant for
391     * \TYPO3\CMS\Core\Resource-internal usage; don't use it to move files.
392     *
393     * @internal Should only be used by other parts of the File API (e.g. drivers after moving a file)
394     * @param ResourceStorage $storage
395     * @return File
396     */
397    public function setStorage(ResourceStorage $storage)
398    {
399        $this->storage = $storage;
400        $this->properties['storage'] = $storage->getUid();
401        return $this;
402    }
403
404    /**
405     * Set the identifier of this file
406     *
407     * @internal Should only be used by other parts of the File API (e.g. drivers after moving a file)
408     * @param string $identifier
409     * @return File
410     */
411    public function setIdentifier($identifier)
412    {
413        $this->identifier = $identifier;
414        return $this;
415    }
416
417    /**
418     * Returns a combined identifier of this file, i.e. the storage UID and the
419     * folder identifier separated by a colon ":".
420     *
421     * @return string Combined storage and file identifier, e.g. StorageUID:path/and/fileName.png
422     */
423    public function getCombinedIdentifier()
424    {
425        if (!empty($this->properties['storage']) && MathUtility::canBeInterpretedAsInteger($this->properties['storage'])) {
426            $combinedIdentifier = $this->properties['storage'] . ':' . $this->getIdentifier();
427        } else {
428            $combinedIdentifier = $this->getStorage()->getUid() . ':' . $this->getIdentifier();
429        }
430        return $combinedIdentifier;
431    }
432
433    /**
434     * Deletes this file from its storage. This also means that this object becomes useless.
435     *
436     * @return bool TRUE if deletion succeeded
437     */
438    public function delete()
439    {
440        // The storage will mark this file as deleted
441        $wasDeleted = $this->getStorage()->deleteFile($this);
442
443        // Unset all properties when deleting the file, as they will be stale anyway
444        // This needs to happen AFTER the storage deleted the file, because the storage
445        // emits a signal, which passes the file object to the slots, which may need
446        // all file properties of the deleted file.
447        $this->properties = [];
448
449        return $wasDeleted;
450    }
451
452    /**
453     * Marks this file as deleted. This should only be used inside the
454     * File Abstraction Layer, as it is a low-level API method.
455     */
456    public function setDeleted()
457    {
458        $this->deleted = true;
459    }
460
461    /**
462     * Returns TRUE if this file has been deleted
463     *
464     * @return bool
465     */
466    public function isDeleted()
467    {
468        return $this->deleted;
469    }
470
471    /**
472     * Renames this file.
473     *
474     * @param string $newName The new file name
475     *
476     * @param string $conflictMode
477     * @return FileInterface
478     */
479    public function rename($newName, $conflictMode = DuplicationBehavior::RENAME)
480    {
481        if ($this->deleted) {
482            throw new \RuntimeException('File has been deleted.', 1329821482);
483        }
484        return $this->getStorage()->renameFile($this, $newName, $conflictMode);
485    }
486
487    /**
488     * Copies this file into a target folder
489     *
490     * @param Folder $targetFolder Folder to copy file into.
491     * @param string $targetFileName an optional destination fileName
492     * @param string $conflictMode a value of the \TYPO3\CMS\Core\Resource\DuplicationBehavior enumeration
493     *
494     * @throws \RuntimeException
495     * @return File The new (copied) file.
496     */
497    public function copyTo(Folder $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
498    {
499        if ($this->deleted) {
500            throw new \RuntimeException('File has been deleted.', 1329821483);
501        }
502        return $targetFolder->getStorage()->copyFile($this, $targetFolder, $targetFileName, $conflictMode);
503    }
504
505    /**
506     * Moves the file into the target folder
507     *
508     * @param Folder $targetFolder Folder to move file into.
509     * @param string $targetFileName an optional destination fileName
510     * @param string $conflictMode a value of the \TYPO3\CMS\Core\Resource\DuplicationBehavior enumeration
511     *
512     * @throws \RuntimeException
513     * @return File This file object, with updated properties.
514     */
515    public function moveTo(Folder $targetFolder, $targetFileName = null, $conflictMode = DuplicationBehavior::RENAME)
516    {
517        if ($this->deleted) {
518            throw new \RuntimeException('File has been deleted.', 1329821484);
519        }
520        return $targetFolder->getStorage()->moveFile($this, $targetFolder, $targetFileName, $conflictMode);
521    }
522
523    /*****************
524     * SPECIAL METHODS
525     *****************/
526    /**
527     * Returns a publicly accessible URL for this file
528     *
529     * WARNING: Access to the file may be restricted by further means, e.g. some
530     * web-based authentication. You have to take care of this yourself.
531     *
532     * @param bool $relativeToCurrentScript Determines whether the URL returned should be relative to the current script, in case it is relative at all (only for the LocalDriver)
533     * @return string|null NULL if file is deleted, the generated URL otherwise
534     */
535    public function getPublicUrl($relativeToCurrentScript = false)
536    {
537        if ($this->deleted) {
538            return null;
539        }
540        return $this->getStorage()->getPublicUrl($this, $relativeToCurrentScript);
541    }
542
543    /**
544     * Returns a path to a local version of this file to process it locally (e.g. with some system tool).
545     * If the file is normally located on a remote storages, this creates a local copy.
546     * If the file is already on the local system, this only makes a new copy if $writable is set to TRUE.
547     *
548     * @param bool $writable Set this to FALSE if you only want to do read operations on the file.
549     *
550     * @throws \RuntimeException
551     * @return string
552     */
553    public function getForLocalProcessing($writable = true)
554    {
555        if ($this->deleted) {
556            throw new \RuntimeException('File has been deleted.', 1329821486);
557        }
558        return $this->getStorage()->getFileForLocalProcessing($this, $writable);
559    }
560
561    /***********************
562     * INDEX RELATED METHODS
563     ***********************/
564    /**
565     * Updates properties of this object.
566     * This method is used to reconstitute settings from the
567     * database into this object after being intantiated.
568     *
569     * @param array $properties
570     */
571    abstract public function updateProperties(array $properties);
572
573    /**
574     * Returns the parent folder.
575     *
576     * @return FolderInterface
577     */
578    public function getParentFolder()
579    {
580        return $this->getStorage()->getFolder($this->getStorage()->getFolderIdentifierFromFileIdentifier($this->getIdentifier()));
581    }
582}
583