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