@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 1858 lines 53 kB view raw
1<?php 2 3/** 4 * Parameters 5 * ========== 6 * 7 * When creating a new file using a method like @{method:newFromFileData}, these 8 * parameters are supported: 9 * 10 * | name | Human readable filename. 11 * | authorPHID | User PHID of uploader. 12 * | ttl.absolute | Temporary file lifetime as an epoch timestamp. 13 * | ttl.relative | Temporary file lifetime, relative to now, in seconds. 14 * | viewPolicy | File visibility policy. 15 * | isExplicitUpload | Used to show users files they explicitly uploaded. 16 * | canCDN | Allows the file to be cached and delivered over a CDN. 17 * | profile | Marks the file as a profile image. 18 * | format | Internal encoding format. 19 * | mime-type | Optional, explicit file MIME type. 20 * | builtin | Optional filename, identifies this as a builtin. 21 * 22 */ 23final class PhabricatorFile extends PhabricatorFileDAO 24 implements 25 PhabricatorApplicationTransactionInterface, 26 PhabricatorTokenReceiverInterface, 27 PhabricatorSubscribableInterface, 28 PhabricatorFlaggableInterface, 29 PhabricatorPolicyInterface, 30 PhabricatorDestructibleInterface, 31 PhabricatorConduitResultInterface, 32 PhabricatorIndexableInterface, 33 PhabricatorNgramsInterface { 34 35 const METADATA_IMAGE_WIDTH = 'width'; 36 const METADATA_IMAGE_HEIGHT = 'height'; 37 const METADATA_CAN_CDN = 'canCDN'; 38 const METADATA_BUILTIN = 'builtin'; 39 const METADATA_PARTIAL = 'partial'; 40 const METADATA_PROFILE = 'profile'; 41 const METADATA_STORAGE = 'storage'; 42 const METADATA_INTEGRITY = 'integrity'; 43 const METADATA_CHUNK = 'chunk'; 44 const METADATA_ALT_TEXT = 'alt'; 45 46 const STATUS_ACTIVE = 'active'; 47 const STATUS_DELETED = 'deleted'; 48 49 protected $name; 50 protected $mimeType; 51 protected $byteSize; 52 protected $authorPHID; 53 protected $secretKey; 54 protected $contentHash; 55 protected $metadata = array(); 56 protected $mailKey; 57 protected $builtinKey; 58 59 protected $storageEngine; 60 protected $storageFormat; 61 protected $storageHandle; 62 63 protected $ttl; 64 protected $isExplicitUpload = 1; 65 protected $viewPolicy = PhabricatorPolicies::POLICY_USER; 66 protected $isPartial = 0; 67 protected $isDeleted = 0; 68 69 private $objects = self::ATTACHABLE; 70 private $objectPHIDs = self::ATTACHABLE; 71 private $originalFile = self::ATTACHABLE; 72 private $transforms = self::ATTACHABLE; 73 74 public static function initializeNewFile() { 75 $app = id(new PhabricatorApplicationQuery()) 76 ->setViewer(PhabricatorUser::getOmnipotentUser()) 77 ->withClasses(array(PhabricatorFilesApplication::class)) 78 ->executeOne(); 79 80 $view_policy = $app->getPolicy( 81 FilesDefaultViewCapability::CAPABILITY); 82 83 return id(new self()) 84 ->setViewPolicy($view_policy) 85 ->setIsPartial(0) 86 ->attachOriginalFile(null) 87 ->attachObjects(array()) 88 ->attachObjectPHIDs(array()); 89 } 90 91 protected function getConfiguration() { 92 return array( 93 self::CONFIG_AUX_PHID => true, 94 self::CONFIG_SERIALIZATION => array( 95 'metadata' => self::SERIALIZATION_JSON, 96 ), 97 self::CONFIG_COLUMN_SCHEMA => array( 98 'name' => 'sort255?', 99 'mimeType' => 'text255?', 100 'byteSize' => 'uint64', 101 'storageEngine' => 'text32', 102 'storageFormat' => 'text32', 103 'storageHandle' => 'text255', 104 'authorPHID' => 'phid?', 105 'secretKey' => 'bytes20?', 106 'contentHash' => 'bytes64?', 107 'ttl' => 'epoch?', 108 'isExplicitUpload' => 'bool?', 109 'mailKey' => 'bytes20', 110 'isPartial' => 'bool', 111 'builtinKey' => 'text64?', 112 'isDeleted' => 'bool', 113 ), 114 self::CONFIG_KEY_SCHEMA => array( 115 'key_phid' => null, 116 'phid' => array( 117 'columns' => array('phid'), 118 'unique' => true, 119 ), 120 'authorPHID' => array( 121 'columns' => array('authorPHID'), 122 ), 123 'contentHash' => array( 124 'columns' => array('contentHash'), 125 ), 126 'key_ttl' => array( 127 'columns' => array('ttl'), 128 ), 129 'key_dateCreated' => array( 130 'columns' => array('dateCreated'), 131 ), 132 'key_partial' => array( 133 'columns' => array('authorPHID', 'isPartial'), 134 ), 135 'key_builtin' => array( 136 'columns' => array('builtinKey'), 137 'unique' => true, 138 ), 139 'key_engine' => array( 140 'columns' => array('storageEngine', 'storageHandle(64)'), 141 ), 142 ), 143 ) + parent::getConfiguration(); 144 } 145 146 public function generatePHID() { 147 return PhabricatorPHID::generateNewPHID( 148 PhabricatorFileFilePHIDType::TYPECONST); 149 } 150 151 public function save() { 152 if (!$this->getSecretKey()) { 153 $this->setSecretKey($this->generateSecretKey()); 154 } 155 if (!$this->getMailKey()) { 156 $this->setMailKey(Filesystem::readRandomCharacters(20)); 157 } 158 return parent::save(); 159 } 160 161 public function saveAndIndex() { 162 $this->save(); 163 164 if ($this->isIndexableFile()) { 165 PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID()); 166 } 167 168 return $this; 169 } 170 171 private function isIndexableFile() { 172 if ($this->getIsChunk()) { 173 return false; 174 } 175 176 return true; 177 } 178 179 /** 180 * Get file monogram in the format of "F123" 181 * @return string 182 */ 183 public function getMonogram() { 184 return 'F'.$this->getID(); 185 } 186 187 public function scrambleSecret() { 188 return $this->setSecretKey($this->generateSecretKey()); 189 } 190 191 public static function readUploadedFileData($spec) { 192 if (!$spec) { 193 throw new Exception(pht('No file was uploaded!')); 194 } 195 196 $err = idx($spec, 'error'); 197 if ($err) { 198 throw new PhabricatorFileUploadException($err); 199 } 200 201 $tmp_name = idx($spec, 'tmp_name'); 202 203 // NOTE: If we parsed the request body ourselves, the files we wrote will 204 // not be registered in the `is_uploaded_file()` list. It's fine to skip 205 // this check: it just protects against sloppy code from the long ago era 206 // of "register_globals". 207 208 if (ini_get('enable_post_data_reading')) { 209 $is_valid = @is_uploaded_file($tmp_name); 210 if (!$is_valid) { 211 throw new Exception(pht('File is not an uploaded file.')); 212 } 213 } 214 215 $file_data = Filesystem::readFile($tmp_name); 216 $file_size = idx($spec, 'size'); 217 218 if (strlen($file_data) != $file_size) { 219 throw new Exception(pht('File size disagrees with uploaded size.')); 220 } 221 222 return $file_data; 223 } 224 225 public static function newFromPHPUpload($spec, array $params = array()) { 226 $file_data = self::readUploadedFileData($spec); 227 228 $file_name = nonempty( 229 idx($params, 'name'), 230 idx($spec, 'name')); 231 $params = array( 232 'name' => $file_name, 233 ) + $params; 234 235 return self::newFromFileData($file_data, $params); 236 } 237 238 public static function newFromXHRUpload($data, array $params = array()) { 239 return self::newFromFileData($data, $params); 240 } 241 242 243 public static function newFileFromContentHash($hash, array $params) { 244 if ($hash === null) { 245 return null; 246 } 247 248 // Check to see if a file with same hash already exists. 249 $file = id(new PhabricatorFile())->loadOneWhere( 250 'contentHash = %s LIMIT 1', 251 $hash); 252 if (!$file) { 253 return null; 254 } 255 256 $copy_of_storage_engine = $file->getStorageEngine(); 257 $copy_of_storage_handle = $file->getStorageHandle(); 258 $copy_of_storage_format = $file->getStorageFormat(); 259 $copy_of_storage_properties = $file->getStorageProperties(); 260 $copy_of_byte_size = $file->getByteSize(); 261 $copy_of_mime_type = $file->getMimeType(); 262 263 $new_file = self::initializeNewFile(); 264 265 $new_file->setByteSize($copy_of_byte_size); 266 267 $new_file->setContentHash($hash); 268 $new_file->setStorageEngine($copy_of_storage_engine); 269 $new_file->setStorageHandle($copy_of_storage_handle); 270 $new_file->setStorageFormat($copy_of_storage_format); 271 $new_file->setStorageProperties($copy_of_storage_properties); 272 $new_file->setMimeType($copy_of_mime_type); 273 $new_file->copyDimensions($file); 274 275 $new_file->readPropertiesFromParameters($params); 276 277 $new_file->saveAndIndex(); 278 279 return $new_file; 280 } 281 282 public static function newChunkedFile( 283 PhabricatorFileStorageEngine $engine, 284 $length, 285 array $params) { 286 287 $file = self::initializeNewFile(); 288 289 $file->setByteSize($length); 290 291 // NOTE: Once we receive the first chunk, we'll detect its MIME type and 292 // update the parent file if a MIME type hasn't been provided. This matters 293 // for large media files like video. 294 $mime_type = idx($params, 'mime-type', ''); 295 if (!strlen($mime_type)) { 296 $file->setMimeType('application/octet-stream'); 297 } 298 299 $chunked_hash = idx($params, 'chunkedHash'); 300 301 // Get rid of this parameter now; we aren't passing it any further down 302 // the stack. 303 unset($params['chunkedHash']); 304 305 if ($chunked_hash) { 306 $file->setContentHash($chunked_hash); 307 } else { 308 // See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some 309 // discussion of this. 310 $seed = Filesystem::readRandomBytes(64); 311 $hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput( 312 $seed); 313 $file->setContentHash($hash); 314 } 315 316 $file->setStorageEngine($engine->getEngineIdentifier()); 317 $file->setStorageHandle(PhabricatorFileChunk::newChunkHandle()); 318 319 // Chunked files are always stored raw because they do not actually store 320 // data. The chunks do, and can be individually formatted. 321 $file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY); 322 323 $file->setIsPartial(1); 324 325 $file->readPropertiesFromParameters($params); 326 327 return $file; 328 } 329 330 private static function buildFromFileData($data, array $params = array()) { 331 332 if (isset($params['storageEngines'])) { 333 $engines = $params['storageEngines']; 334 } else { 335 $size = strlen($data); 336 $engines = PhabricatorFileStorageEngine::loadStorageEngines($size); 337 338 if (!$engines) { 339 throw new Exception( 340 pht( 341 'No configured storage engine can store this file. See '. 342 '"Configuring File Storage" in the documentation for '. 343 'information on configuring storage engines. '. 344 'This is likely because the file is too large.')); 345 } 346 } 347 348 assert_instances_of($engines, PhabricatorFileStorageEngine::class); 349 if (!$engines) { 350 throw new Exception(pht('No valid storage engines are available!')); 351 } 352 353 $file = self::initializeNewFile(); 354 355 $aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY; 356 $has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type); 357 if ($has_aes !== null) { 358 $default_key = PhabricatorFileAES256StorageFormat::FORMATKEY; 359 } else { 360 $default_key = PhabricatorFileRawStorageFormat::FORMATKEY; 361 } 362 $key = idx($params, 'format', $default_key); 363 364 // Callers can pass in an object explicitly instead of a key. This is 365 // primarily useful for unit tests. 366 if ($key instanceof PhabricatorFileStorageFormat) { 367 $format = clone $key; 368 } else { 369 $format = clone PhabricatorFileStorageFormat::requireFormat($key); 370 } 371 372 $format->setFile($file); 373 374 $properties = $format->newStorageProperties(); 375 $file->setStorageFormat($format->getStorageFormatKey()); 376 $file->setStorageProperties($properties); 377 378 $data_handle = null; 379 $engine_identifier = null; 380 $integrity_hash = null; 381 $exceptions = array(); 382 foreach ($engines as $engine) { 383 $engine_class = get_class($engine); 384 try { 385 $result = $file->writeToEngine( 386 $engine, 387 $data, 388 $params); 389 390 list($engine_identifier, $data_handle, $integrity_hash) = $result; 391 392 // We stored the file somewhere so stop trying to write it to other 393 // places. 394 break; 395 } catch (PhabricatorFileStorageConfigurationException $ex) { 396 // If an engine is outright misconfigured (or misimplemented), raise 397 // that immediately since it probably needs attention. 398 throw $ex; 399 } catch (Throwable $ex) { 400 phlog($ex); 401 402 // If an engine doesn't work, keep trying all the other valid engines 403 // in case something else works. 404 $exceptions[$engine_class] = $ex; 405 } 406 } 407 408 if (!$data_handle) { 409 throw new PhutilAggregateException( 410 pht('All storage engines failed to write file:'), 411 $exceptions); 412 } 413 414 $file->setByteSize(strlen($data)); 415 416 $hash = self::hashFileContent($data); 417 $file->setContentHash($hash); 418 419 $file->setStorageEngine($engine_identifier); 420 $file->setStorageHandle($data_handle); 421 422 $file->setIntegrityHash($integrity_hash); 423 424 $file->readPropertiesFromParameters($params); 425 426 if (!$file->getMimeType()) { 427 $tmp = new TempFile(); 428 Filesystem::writeFile($tmp, $data); 429 $file->setMimeType(Filesystem::getMimeType($tmp)); 430 unset($tmp); 431 } 432 433 try { 434 $file->updateDimensions(false); 435 } catch (Throwable $ex) { 436 // Do nothing. 437 } 438 439 $file->saveAndIndex(); 440 441 return $file; 442 } 443 444 public static function newFromFileData($data, array $params = array()) { 445 $hash = self::hashFileContent($data); 446 447 if ($hash !== null) { 448 $file = self::newFileFromContentHash($hash, $params); 449 if ($file) { 450 return $file; 451 } 452 } 453 454 return self::buildFromFileData($data, $params); 455 } 456 457 public function migrateToEngine( 458 PhabricatorFileStorageEngine $engine, 459 $make_copy) { 460 461 if (!$this->getID() || !$this->getStorageHandle()) { 462 throw new Exception( 463 pht("You can not migrate a file which hasn't yet been saved.")); 464 } 465 466 $data = $this->loadFileData(); 467 $params = array( 468 'name' => $this->getName(), 469 ); 470 471 list($new_identifier, $new_handle, $integrity_hash) = $this->writeToEngine( 472 $engine, 473 $data, 474 $params); 475 476 $old_engine = $this->instantiateStorageEngine(); 477 $old_identifier = $this->getStorageEngine(); 478 $old_handle = $this->getStorageHandle(); 479 480 $this->setStorageEngine($new_identifier); 481 $this->setStorageHandle($new_handle); 482 $this->setIntegrityHash($integrity_hash); 483 $this->save(); 484 485 if (!$make_copy) { 486 $this->deleteFileDataIfUnused( 487 $old_engine, 488 $old_identifier, 489 $old_handle); 490 } 491 492 return $this; 493 } 494 495 public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) { 496 if (!$this->getID() || !$this->getStorageHandle()) { 497 throw new Exception( 498 pht("You can not migrate a file which hasn't yet been saved.")); 499 } 500 501 $data = $this->loadFileData(); 502 $params = array( 503 'name' => $this->getName(), 504 ); 505 506 $engine = $this->instantiateStorageEngine(); 507 $old_handle = $this->getStorageHandle(); 508 509 $properties = $format->newStorageProperties(); 510 $this->setStorageFormat($format->getStorageFormatKey()); 511 $this->setStorageProperties($properties); 512 513 list($identifier, $new_handle, $integrity_hash) = $this->writeToEngine( 514 $engine, 515 $data, 516 $params); 517 518 $this->setStorageHandle($new_handle); 519 $this->setIntegrityHash($integrity_hash); 520 $this->save(); 521 522 $this->deleteFileDataIfUnused( 523 $engine, 524 $identifier, 525 $old_handle); 526 527 return $this; 528 } 529 530 public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) { 531 if (!$this->getID() || !$this->getStorageHandle()) { 532 throw new Exception( 533 pht("You can not cycle keys for a file which hasn't yet been saved.")); 534 } 535 536 $properties = $format->cycleStorageProperties(); 537 $this->setStorageProperties($properties); 538 $this->save(); 539 540 return $this; 541 } 542 543 private function writeToEngine( 544 PhabricatorFileStorageEngine $engine, 545 $data, 546 array $params) { 547 548 $engine_class = get_class($engine); 549 550 $format = $this->newStorageFormat(); 551 552 $data_iterator = array($data); 553 $formatted_iterator = $format->newWriteIterator($data_iterator); 554 $formatted_data = $this->loadDataFromIterator($formatted_iterator); 555 556 $integrity_hash = $engine->newIntegrityHash($formatted_data, $format); 557 558 $data_handle = $engine->writeFile($formatted_data, $params); 559 560 if (!$data_handle || strlen($data_handle) > 255) { 561 // This indicates an improperly implemented storage engine. 562 throw new PhabricatorFileStorageConfigurationException( 563 pht( 564 "Storage engine '%s' executed %s but did not return a valid ". 565 "handle ('%s') to the data: it must be nonempty and no longer ". 566 "than 255 characters.", 567 $engine_class, 568 'writeFile()', 569 $data_handle)); 570 } 571 572 $engine_identifier = $engine->getEngineIdentifier(); 573 if (!$engine_identifier || strlen($engine_identifier) > 32) { 574 throw new PhabricatorFileStorageConfigurationException( 575 pht( 576 "Storage engine '%s' returned an improper engine identifier '{%s}': ". 577 "it must be nonempty and no longer than 32 characters.", 578 $engine_class, 579 $engine_identifier)); 580 } 581 582 return array($engine_identifier, $data_handle, $integrity_hash); 583 } 584 585 586 /** 587 * Download a remote resource over HTTP and save the response body as a file. 588 * 589 * This method respects `security.outbound-blacklist`, and protects against 590 * HTTP redirection (by manually following "Location" headers and verifying 591 * each destination). It does not protect against DNS rebinding. See 592 * discussion in T6755. 593 */ 594 public static function newFromFileDownload($uri, array $params = array()) { 595 $timeout = 5; 596 597 $redirects = array(); 598 $current = $uri; 599 while (true) { 600 try { 601 if (count($redirects) > 10) { 602 throw new Exception( 603 pht('Too many redirects trying to fetch remote URI.')); 604 } 605 606 $resolved = PhabricatorEnv::requireValidRemoteURIForFetch( 607 $current, 608 array( 609 'http', 610 'https', 611 )); 612 613 list($resolved_uri, $resolved_domain) = $resolved; 614 615 $current = new PhutilURI($current); 616 if ($current->getProtocol() == 'http') { 617 // For HTTP, we can use a pre-resolved URI to defuse DNS rebinding. 618 $fetch_uri = $resolved_uri; 619 $fetch_host = $resolved_domain; 620 } else { 621 // For HTTPS, we can't: cURL won't verify the SSL certificate if 622 // the domain has been replaced with an IP. But internal services 623 // presumably will not have valid certificates for rebindable 624 // domain names on attacker-controlled domains, so the DNS rebinding 625 // attack should generally not be possible anyway. 626 $fetch_uri = $current; 627 $fetch_host = null; 628 } 629 630 $future = id(new HTTPSFuture($fetch_uri)) 631 ->setFollowLocation(false) 632 ->setTimeout($timeout); 633 634 if ($fetch_host !== null) { 635 $future->addHeader('Host', $fetch_host); 636 } 637 638 list($status, $body, $headers) = $future->resolve(); 639 640 if ($status->isRedirect()) { 641 // This is an HTTP 3XX status, so look for a "Location" header. 642 $location = null; 643 foreach ($headers as $header) { 644 list($name, $value) = $header; 645 if (phutil_utf8_strtolower($name) == 'location') { 646 $location = $value; 647 break; 648 } 649 } 650 651 // HTTP 3XX status with no "Location" header, just treat this like 652 // a normal HTTP error. 653 if ($location === null) { 654 throw $status; 655 } 656 657 if (isset($redirects[$location])) { 658 throw new Exception( 659 pht('Encountered loop while following redirects.')); 660 } 661 662 $redirects[$location] = $location; 663 $current = $location; 664 // We'll fall off the bottom and go try this URI now. 665 } else if ($status->isError()) { 666 // This is something other than an HTTP 2XX or HTTP 3XX status, so 667 // just bail out. 668 throw $status; 669 } else { 670 // This is HTTP 2XX, so use the response body to save the file data. 671 // Provide a default name based on the URI, truncating it if the URI 672 // is exceptionally long. 673 674 $default_name = basename($uri); 675 $default_name = id(new PhutilUTF8StringTruncator()) 676 ->setMaximumBytes(64) 677 ->truncateString($default_name); 678 679 $params = $params + array( 680 'name' => $default_name, 681 ); 682 683 return self::newFromFileData($body, $params); 684 } 685 } catch (Throwable $ex) { 686 if ($redirects) { 687 throw new Exception( 688 pht( 689 'Failed to fetch remote URI "%s" after following %s redirect(s) '. 690 '(%s): %s', 691 $uri, 692 phutil_count($redirects), 693 implode(' > ', array_keys($redirects)), 694 $ex->getMessage()), 695 0, 696 $ex); 697 } else { 698 throw $ex; 699 } 700 } 701 } 702 } 703 704 public static function normalizeFileName($file_name) { 705 $pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@"; 706 $file_name = preg_replace($pattern, '_', $file_name); 707 $file_name = preg_replace('@_+@', '_', $file_name); 708 $file_name = trim($file_name, '_'); 709 710 $disallowed_filenames = array( 711 '.' => 'dot', 712 '..' => 'dotdot', 713 '' => 'file', 714 ); 715 $file_name = idx($disallowed_filenames, $file_name, $file_name); 716 717 return $file_name; 718 } 719 720 public function delete() { 721 // We want to delete all the rows which mark this file as the transformation 722 // of some other file (since we're getting rid of it). We also delete all 723 // the transformations of this file, so that a user who deletes an image 724 // doesn't need to separately hunt down and delete a bunch of thumbnails and 725 // resizes of it. 726 727 $outbound_xforms = id(new PhabricatorFileQuery()) 728 ->setViewer(PhabricatorUser::getOmnipotentUser()) 729 ->withTransforms( 730 array( 731 array( 732 'originalPHID' => $this->getPHID(), 733 'transform' => true, 734 ), 735 )) 736 ->execute(); 737 738 foreach ($outbound_xforms as $outbound_xform) { 739 $outbound_xform->delete(); 740 } 741 742 $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 743 'transformedPHID = %s', 744 $this->getPHID()); 745 746 $this->openTransaction(); 747 foreach ($inbound_xforms as $inbound_xform) { 748 $inbound_xform->delete(); 749 } 750 $ret = parent::delete(); 751 $this->saveTransaction(); 752 753 $this->deleteFileDataIfUnused( 754 $this->instantiateStorageEngine(), 755 $this->getStorageEngine(), 756 $this->getStorageHandle()); 757 758 return $ret; 759 } 760 761 762 /** 763 * Destroy stored file data if there are no remaining files which reference 764 * it. 765 */ 766 public function deleteFileDataIfUnused( 767 PhabricatorFileStorageEngine $engine, 768 $engine_identifier, 769 $handle) { 770 771 // Check to see if any files are using storage. 772 $usage = id(new self())->loadAllWhere( 773 'storageEngine = %s AND storageHandle = %s LIMIT 1', 774 $engine_identifier, 775 $handle); 776 777 // If there are no files using the storage, destroy the actual storage. 778 if (!$usage) { 779 try { 780 $engine->deleteFile($handle); 781 } catch (Throwable $ex) { 782 // In the worst case, we're leaving some data stranded in a storage 783 // engine, which is not a big deal. 784 phlog($ex); 785 } 786 } 787 } 788 789 public static function hashFileContent($data) { 790 // NOTE: Hashing can fail if the algorithm isn't available in the current 791 // build of PHP. It's fine if we're unable to generate a content hash: 792 // it just means we'll store extra data when users upload duplicate files 793 // instead of being able to deduplicate it. 794 795 $hash = hash('sha256', $data, $raw_output = false); 796 if ($hash === false) { 797 return null; 798 } 799 800 return $hash; 801 } 802 803 public function loadFileData() { 804 $iterator = $this->getFileDataIterator(); 805 return $this->loadDataFromIterator($iterator); 806 } 807 808 809 /** 810 * Return an iterable which emits file content bytes. 811 * 812 * @param int $begin (optional) Offset for the start of data. 813 * @param int $end (optional) Offset for the end of data. 814 * @return Iterable Iterable object which emits requested data. 815 */ 816 public function getFileDataIterator($begin = null, $end = null) { 817 $engine = $this->instantiateStorageEngine(); 818 819 $format = $this->newStorageFormat(); 820 821 $iterator = $engine->getRawFileDataIterator( 822 $this, 823 $begin, 824 $end, 825 $format); 826 827 return $iterator; 828 } 829 830 /** 831 * Get file URI in the format of "/F123" 832 * @return string 833 */ 834 public function getURI() { 835 return $this->getInfoURI(); 836 } 837 838 /** 839 * Get file view URI in the format of 840 * https://phorge.example.com/file/data/foo/PHID-FILE-bar/filename 841 * @return string 842 */ 843 public function getViewURI() { 844 if (!$this->getPHID()) { 845 throw new Exception( 846 pht('You must save a file before you can generate a view URI.')); 847 } 848 849 return $this->getCDNURI('data'); 850 } 851 852 /** 853 * Get file view URI in the format of 854 * https://phorge.example.com/file/data/foo/PHID-FILE-bar/filename or 855 * https://phorge.example.com/file/download/foo/PHID-FILE-bar/filename 856 * @return string 857 */ 858 public function getCDNURI($request_kind) { 859 if (($request_kind !== 'data') && 860 ($request_kind !== 'download')) { 861 throw new Exception( 862 pht( 863 'Unknown file content request kind "%s".', 864 $request_kind)); 865 } 866 867 $name = self::normalizeFileName($this->getName()); 868 $name = phutil_escape_uri($name); 869 870 $parts = array(); 871 $parts[] = 'file'; 872 $parts[] = $request_kind; 873 874 // If this is an instanced install, add the instance identifier to the URI. 875 // Instanced configurations behind a CDN may not be able to control the 876 // request domain used by the CDN (as with AWS CloudFront). Embedding the 877 // instance identity in the path allows us to distinguish between requests 878 // originating from different instances but served through the same CDN. 879 $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); 880 if (phutil_nonempty_string($instance)) { 881 $parts[] = '@'.$instance; 882 } 883 884 $parts[] = $this->getSecretKey(); 885 $parts[] = $this->getPHID(); 886 $parts[] = $name; 887 888 $path = '/'.implode('/', $parts); 889 890 // If this file is only partially uploaded, we're just going to return a 891 // local URI to make sure that Ajax works, since the page is inevitably 892 // going to give us an error back. 893 if ($this->getIsPartial()) { 894 return PhabricatorEnv::getURI($path); 895 } else { 896 return PhabricatorEnv::getCDNURI($path); 897 } 898 } 899 900 /** 901 * Get file info URI in the format of "/F123" 902 * @return string 903 */ 904 public function getInfoURI() { 905 return '/'.$this->getMonogram(); 906 } 907 908 public function getBestURI() { 909 if ($this->isViewableInBrowser()) { 910 return $this->getViewURI(); 911 } else { 912 return $this->getInfoURI(); 913 } 914 } 915 916 /** 917 * Get file view URI in the format of 918 * https://phorge.example.com/file/download/foo/PHID-FILE-bar/filename 919 * @return string 920 */ 921 public function getDownloadURI() { 922 return $this->getCDNURI('download'); 923 } 924 925 public function getURIForTransform(PhabricatorFileTransform $transform) { 926 return $this->getTransformedURI($transform->getTransformKey()); 927 } 928 929 private function getTransformedURI($transform) { 930 $parts = array(); 931 $parts[] = 'file'; 932 $parts[] = 'xform'; 933 934 $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); 935 if (phutil_nonempty_string($instance)) { 936 $parts[] = '@'.$instance; 937 } 938 939 $parts[] = $transform; 940 $parts[] = $this->getPHID(); 941 $parts[] = $this->getSecretKey(); 942 943 $path = implode('/', $parts); 944 $path = $path.'/'; 945 946 return PhabricatorEnv::getCDNURI($path); 947 } 948 949 /** 950 * Whether the file can be viewed in a browser 951 * @return bool True if MIME type of the file is listed in the 952 * files.viewable-mime-types setting 953 */ 954 public function isViewableInBrowser() { 955 return ($this->getViewableMimeType() !== null); 956 } 957 958 /** 959 * Whether the file is an image viewable in the browser 960 * @return bool True if MIME type of the file is listed in the 961 * files.image-mime-types setting and file is viewable in the browser 962 */ 963 public function isViewableImage() { 964 if (!$this->isViewableInBrowser()) { 965 return false; 966 } 967 968 $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); 969 $mime_type = $this->getMimeType(); 970 return idx($mime_map, $mime_type); 971 } 972 973 /** 974 * Whether the file is an audio file 975 * @return bool True if MIME type of the file is listed in the 976 * files.audio-mime-types setting and file is viewable in the browser 977 */ 978 public function isAudio() { 979 if (!$this->isViewableInBrowser()) { 980 return false; 981 } 982 983 $mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); 984 $mime_type = $this->getMimeType(); 985 return idx($mime_map, $mime_type); 986 } 987 988 /** 989 * Whether the file is a video file 990 * @return bool True if MIME type of the file is listed in the 991 * files.video-mime-types setting and file is viewable in the browser 992 */ 993 public function isVideo() { 994 if (!$this->isViewableInBrowser()) { 995 return false; 996 } 997 998 $mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types'); 999 $mime_type = $this->getMimeType(); 1000 return idx($mime_map, $mime_type); 1001 } 1002 1003 /** 1004 * Whether the file is a PDF file 1005 * @return bool True if MIME type of the file is application/pdf and file is 1006 * viewable in the browser 1007 */ 1008 public function isPDF() { 1009 if (!$this->isViewableInBrowser()) { 1010 return false; 1011 } 1012 1013 $mime_map = array( 1014 'application/pdf' => 'application/pdf', 1015 ); 1016 1017 $mime_type = $this->getMimeType(); 1018 return idx($mime_map, $mime_type); 1019 } 1020 1021 public function isTransformableImage() { 1022 // NOTE: The way the 'gd' extension works in PHP is that you can install it 1023 // with support for only some file types, so it might be able to handle 1024 // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup 1025 // warns you if you don't have complete support. 1026 1027 $matches = null; 1028 $ok = false; 1029 if ($this->getViewableMimeType() !== null) { 1030 $ok = preg_match( 1031 '@^image/(gif|png|jpe?g|webp)@', 1032 $this->getViewableMimeType(), 1033 $matches); 1034 } 1035 if (!$ok) { 1036 return false; 1037 } 1038 1039 switch ($matches[1]) { 1040 case 'jpg': 1041 case 'jpeg': 1042 return function_exists('imagejpeg'); 1043 case 'png': 1044 return function_exists('imagepng'); 1045 case 'gif': 1046 return function_exists('imagegif'); 1047 case 'webp': 1048 return function_exists('imagewebp'); 1049 default: 1050 throw new Exception(pht('Unknown type matched as image MIME type.')); 1051 } 1052 } 1053 1054 public static function getTransformableImageFormats() { 1055 $supported = array(); 1056 1057 if (function_exists('imagejpeg')) { 1058 $supported[] = 'jpg'; 1059 } 1060 1061 if (function_exists('imagepng')) { 1062 $supported[] = 'png'; 1063 } 1064 1065 if (function_exists('imagegif')) { 1066 $supported[] = 'gif'; 1067 } 1068 1069 if (function_exists('imagewebp')) { 1070 $supported[] = 'webp'; 1071 } 1072 1073 return $supported; 1074 } 1075 1076 public function getDragAndDropDictionary() { 1077 return array( 1078 'id' => $this->getID(), 1079 'phid' => $this->getPHID(), 1080 'uri' => $this->getBestURI(), 1081 ); 1082 } 1083 1084 public function instantiateStorageEngine() { 1085 return self::buildEngine($this->getStorageEngine()); 1086 } 1087 1088 public static function buildEngine($engine_identifier) { 1089 $engines = self::buildAllEngines(); 1090 foreach ($engines as $engine) { 1091 if ($engine->getEngineIdentifier() == $engine_identifier) { 1092 return $engine; 1093 } 1094 } 1095 1096 throw new Exception( 1097 pht( 1098 "Storage engine '%s' could not be located!", 1099 $engine_identifier)); 1100 } 1101 1102 public static function buildAllEngines() { 1103 return id(new PhutilClassMapQuery()) 1104 ->setAncestorClass(PhabricatorFileStorageEngine::class) 1105 ->execute(); 1106 } 1107 1108 /** 1109 * Whether the file is listed as a viewable MIME type 1110 * @return bool True if MIME type of the file is listed in the 1111 * files.viewable-mime-types setting 1112 */ 1113 public function getViewableMimeType() { 1114 $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); 1115 1116 $mime_type = $this->getMimeType(); 1117 $mime_parts = explode(';', $mime_type); 1118 $mime_type = trim(reset($mime_parts)); 1119 1120 return idx($mime_map, $mime_type); 1121 } 1122 1123 public function getDisplayIconForMimeType() { 1124 $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types'); 1125 $mime_type = $this->getMimeType(); 1126 return idx($mime_map, $mime_type, 'fa-file-o'); 1127 } 1128 1129 public function validateSecretKey($key) { 1130 return ($key == $this->getSecretKey()); 1131 } 1132 1133 public function generateSecretKey() { 1134 return Filesystem::readRandomCharacters(20); 1135 } 1136 1137 public function setStorageProperties(array $properties) { 1138 $this->metadata[self::METADATA_STORAGE] = $properties; 1139 return $this; 1140 } 1141 1142 public function getStorageProperties() { 1143 return idx($this->metadata, self::METADATA_STORAGE, array()); 1144 } 1145 1146 public function getStorageProperty($key, $default = null) { 1147 $properties = $this->getStorageProperties(); 1148 return idx($properties, $key, $default); 1149 } 1150 1151 public function loadDataFromIterator($iterator) { 1152 $result = ''; 1153 1154 foreach ($iterator as $chunk) { 1155 $result .= $chunk; 1156 } 1157 1158 return $result; 1159 } 1160 1161 public function updateDimensions($save = true) { 1162 if (!$this->isViewableImage()) { 1163 throw new Exception(pht('This file is not a viewable image.')); 1164 } 1165 1166 if (!function_exists('imagecreatefromstring')) { 1167 throw new Exception(pht('Cannot retrieve image information.')); 1168 } 1169 1170 if ($this->getIsChunk()) { 1171 throw new Exception( 1172 pht('Refusing to assess image dimensions of file chunk.')); 1173 } 1174 1175 $engine = $this->instantiateStorageEngine(); 1176 if ($engine->isChunkEngine()) { 1177 throw new Exception( 1178 pht('Refusing to assess image dimensions of chunked file.')); 1179 } 1180 1181 $data = $this->loadFileData(); 1182 1183 $img = @imagecreatefromstring($data); 1184 if ($img === false) { 1185 throw new Exception(pht('Error when decoding image.')); 1186 } 1187 1188 $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); 1189 $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); 1190 1191 if ($save) { 1192 $this->save(); 1193 } 1194 1195 return $this; 1196 } 1197 1198 public function copyDimensions(PhabricatorFile $file) { 1199 $metadata = $file->getMetadata(); 1200 $width = idx($metadata, self::METADATA_IMAGE_WIDTH); 1201 if ($width) { 1202 $this->metadata[self::METADATA_IMAGE_WIDTH] = $width; 1203 } 1204 $height = idx($metadata, self::METADATA_IMAGE_HEIGHT); 1205 if ($height) { 1206 $this->metadata[self::METADATA_IMAGE_HEIGHT] = $height; 1207 } 1208 1209 return $this; 1210 } 1211 1212 1213 /** 1214 * Load (or build) the @{class:PhabricatorFile} objects for builtin file 1215 * resources. The builtin mechanism allows files shipped with Phabricator 1216 * to be treated like normal files so that APIs do not need to special case 1217 * things like default images or deleted files. 1218 * 1219 * Builtins are located in `resources/builtin/` and identified by their 1220 * name. 1221 * 1222 * @param PhabricatorUser $user Viewing user. 1223 * @param list<PhabricatorFilesBuiltinFile> $builtins List of builtin file 1224 * specs. 1225 * @return array<string, PhabricatorFile> Dictionary of named builtins. 1226 */ 1227 public static function loadBuiltins(PhabricatorUser $user, array $builtins) { 1228 $builtins = mpull($builtins, null, 'getBuiltinFileKey'); 1229 1230 // NOTE: Anyone is allowed to access builtin files. 1231 1232 $files = id(new PhabricatorFileQuery()) 1233 ->setViewer(PhabricatorUser::getOmnipotentUser()) 1234 ->withBuiltinKeys(array_keys($builtins)) 1235 ->execute(); 1236 1237 $results = array(); 1238 foreach ($files as $file) { 1239 $builtin_key = $file->getBuiltinName(); 1240 if ($builtin_key !== null) { 1241 $results[$builtin_key] = $file; 1242 } 1243 } 1244 1245 $build = array(); 1246 foreach ($builtins as $key => $builtin) { 1247 if (isset($results[$key])) { 1248 continue; 1249 } 1250 1251 $data = $builtin->loadBuiltinFileData(); 1252 1253 $params = array( 1254 'name' => $builtin->getBuiltinDisplayName(), 1255 'canCDN' => true, 1256 'builtin' => $key, 1257 ); 1258 1259 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 1260 try { 1261 $file = self::newFromFileData($data, $params); 1262 } catch (AphrontDuplicateKeyQueryException $ex) { 1263 $file = id(new PhabricatorFileQuery()) 1264 ->setViewer(PhabricatorUser::getOmnipotentUser()) 1265 ->withBuiltinKeys(array($key)) 1266 ->executeOne(); 1267 if (!$file) { 1268 throw new Exception( 1269 pht( 1270 'Collided mid-air when generating builtin file "%s", but '. 1271 'then failed to load the object we collided with.', 1272 $key)); 1273 } 1274 } 1275 unset($unguarded); 1276 1277 $file->attachObjectPHIDs(array()); 1278 $file->attachObjects(array()); 1279 1280 $results[$key] = $file; 1281 } 1282 1283 return $results; 1284 } 1285 1286 1287 /** 1288 * Convenience wrapper for @{method:loadBuiltins}. 1289 * 1290 * @param PhabricatorUser $user Viewing user. 1291 * @param string $name Single builtin name to load. 1292 * @return PhabricatorFile Corresponding builtin file. 1293 */ 1294 public static function loadBuiltin(PhabricatorUser $user, $name) { 1295 $builtin = id(new PhabricatorFilesOnDiskBuiltinFile()) 1296 ->setName($name); 1297 1298 $key = $builtin->getBuiltinFileKey(); 1299 1300 return idx(self::loadBuiltins($user, array($builtin)), $key); 1301 } 1302 1303 public function getObjects() { 1304 return $this->assertAttached($this->objects); 1305 } 1306 1307 public function attachObjects(array $objects) { 1308 $this->objects = $objects; 1309 return $this; 1310 } 1311 1312 public function getObjectPHIDs() { 1313 return $this->assertAttached($this->objectPHIDs); 1314 } 1315 1316 public function attachObjectPHIDs(array $object_phids) { 1317 $this->objectPHIDs = $object_phids; 1318 return $this; 1319 } 1320 1321 public function getOriginalFile() { 1322 return $this->assertAttached($this->originalFile); 1323 } 1324 1325 public function attachOriginalFile(?PhabricatorFile $file = null) { 1326 $this->originalFile = $file; 1327 return $this; 1328 } 1329 1330 public function getImageHeight() { 1331 if (!$this->isViewableImage()) { 1332 return null; 1333 } 1334 return idx($this->metadata, self::METADATA_IMAGE_HEIGHT); 1335 } 1336 1337 public function getImageWidth() { 1338 if (!$this->isViewableImage()) { 1339 return null; 1340 } 1341 return idx($this->metadata, self::METADATA_IMAGE_WIDTH); 1342 } 1343 1344 public function getAltText() { 1345 $alt = $this->getCustomAltText(); 1346 1347 if (phutil_nonempty_string($alt)) { 1348 return $alt; 1349 } 1350 1351 return $this->getDefaultAltText(); 1352 } 1353 1354 public function getCustomAltText() { 1355 return idx($this->metadata, self::METADATA_ALT_TEXT); 1356 } 1357 1358 public function setCustomAltText($value) { 1359 $value = phutil_string_cast($value); 1360 1361 if (!strlen($value)) { 1362 $value = null; 1363 } 1364 1365 if ($value === null) { 1366 unset($this->metadata[self::METADATA_ALT_TEXT]); 1367 } else { 1368 $this->metadata[self::METADATA_ALT_TEXT] = $value; 1369 } 1370 1371 return $this; 1372 } 1373 1374 public function getDefaultAltText() { 1375 $parts = array(); 1376 1377 $name = $this->getName(); 1378 if (strlen($name)) { 1379 $parts[] = $name; 1380 } 1381 1382 $stats = array(); 1383 1384 $image_x = $this->getImageWidth(); 1385 $image_y = $this->getImageHeight(); 1386 1387 if ($image_x && $image_y) { 1388 $stats[] = pht( 1389 "%s\xC3\x97%s px", 1390 new PhutilNumber($image_x), 1391 new PhutilNumber($image_y)); 1392 } 1393 1394 $bytes = $this->getByteSize(); 1395 if ($bytes) { 1396 $stats[] = phutil_format_bytes($bytes); 1397 } 1398 1399 if ($stats) { 1400 $parts[] = pht('(%s)', implode(', ', $stats)); 1401 } 1402 1403 if (!$parts) { 1404 return null; 1405 } 1406 1407 return implode(' ', $parts); 1408 } 1409 1410 public function getCanCDN() { 1411 if (!$this->isViewableImage()) { 1412 return false; 1413 } 1414 1415 return idx($this->metadata, self::METADATA_CAN_CDN); 1416 } 1417 1418 public function setCanCDN($can_cdn) { 1419 $this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0; 1420 return $this; 1421 } 1422 1423 public function isBuiltin() { 1424 return ($this->getBuiltinName() !== null); 1425 } 1426 1427 public function getBuiltinName() { 1428 return idx($this->metadata, self::METADATA_BUILTIN); 1429 } 1430 1431 public function setBuiltinName($name) { 1432 $this->metadata[self::METADATA_BUILTIN] = $name; 1433 return $this; 1434 } 1435 1436 public function getIsProfileImage() { 1437 return idx($this->metadata, self::METADATA_PROFILE); 1438 } 1439 1440 public function setIsProfileImage($value) { 1441 $this->metadata[self::METADATA_PROFILE] = $value; 1442 return $this; 1443 } 1444 1445 public function getIsChunk() { 1446 return idx($this->metadata, self::METADATA_CHUNK); 1447 } 1448 1449 public function setIsChunk($value) { 1450 $this->metadata[self::METADATA_CHUNK] = $value; 1451 return $this; 1452 } 1453 1454 public function setIntegrityHash($integrity_hash) { 1455 $this->metadata[self::METADATA_INTEGRITY] = $integrity_hash; 1456 return $this; 1457 } 1458 1459 public function getIntegrityHash() { 1460 return idx($this->metadata, self::METADATA_INTEGRITY); 1461 } 1462 1463 public function newIntegrityHash() { 1464 $engine = $this->instantiateStorageEngine(); 1465 1466 if ($engine->isChunkEngine()) { 1467 return null; 1468 } 1469 1470 $format = $this->newStorageFormat(); 1471 1472 $storage_handle = $this->getStorageHandle(); 1473 $data = $engine->readFile($storage_handle); 1474 1475 return $engine->newIntegrityHash($data, $format); 1476 } 1477 1478 /** 1479 * Write the policy edge between this file and some object. 1480 * This method is successful even if the file is already attached. 1481 * 1482 * @param string $phid Object PHID to attach to. 1483 * @return $this 1484 */ 1485 public function attachToObject($phid) { 1486 self::attachFileToObject($this->getPHID(), $phid); 1487 return $this; 1488 } 1489 1490 /** 1491 * Write the policy edge between a file and some object. 1492 * This method is successful even if the file is already attached. 1493 * NOTE: Please avoid to use this static method directly. 1494 * Instead, use PhabricatorFile#attachToObject(phid). 1495 * 1496 * @param string $file_phid File PHID to attach from. 1497 * @param string $object_phid Object PHID to attach to. 1498 * @return void 1499 */ 1500 public static function attachFileToObject($file_phid, $object_phid) { 1501 1502 // It can be easy to confuse the two arguments. Be strict. 1503 if (phid_get_type($file_phid) !== PhabricatorFileFilePHIDType::TYPECONST) { 1504 throw new Exception(pht('The first argument must be a phid of a file.')); 1505 } 1506 1507 $attachment_table = new PhabricatorFileAttachment(); 1508 $attachment_conn = $attachment_table->establishConnection('w'); 1509 1510 queryfx( 1511 $attachment_conn, 1512 'INSERT INTO %R (objectPHID, filePHID, attachmentMode, 1513 attacherPHID, dateCreated, dateModified) 1514 VALUES (%s, %s, %s, %ns, %d, %d) 1515 ON DUPLICATE KEY UPDATE 1516 attachmentMode = VALUES(attachmentMode), 1517 attacherPHID = VALUES(attacherPHID), 1518 dateModified = VALUES(dateModified)', 1519 $attachment_table, 1520 $object_phid, 1521 $file_phid, 1522 PhabricatorFileAttachment::MODE_ATTACH, 1523 null, 1524 PhabricatorTime::getNow(), 1525 PhabricatorTime::getNow()); 1526 } 1527 1528 1529 /** 1530 * Configure a newly created file object according to specified parameters. 1531 * 1532 * This method is called both when creating a file from fresh data, and 1533 * when creating a new file which reuses existing storage. 1534 * 1535 * @param map<string, mixed> $params Bag of parameters, see 1536 * @{class:PhabricatorFile} for documentation. 1537 * @return $this 1538 */ 1539 private function readPropertiesFromParameters(array $params) { 1540 PhutilTypeSpec::checkMap( 1541 $params, 1542 array( 1543 'name' => 'optional string', 1544 'authorPHID' => 'optional string', 1545 'ttl.relative' => 'optional int', 1546 'ttl.absolute' => 'optional int', 1547 'viewPolicy' => 'optional string', 1548 'isExplicitUpload' => 'optional bool', 1549 'canCDN' => 'optional bool', 1550 'profile' => 'optional bool', 1551 'format' => 'optional string|PhabricatorFileStorageFormat', 1552 'mime-type' => 'optional string', 1553 'builtin' => 'optional string', 1554 'storageEngines' => 'optional list<PhabricatorFileStorageEngine>', 1555 'chunk' => 'optional bool', 1556 )); 1557 1558 $file_name = idx($params, 'name'); 1559 $this->setName($file_name); 1560 1561 $author_phid = idx($params, 'authorPHID'); 1562 $this->setAuthorPHID($author_phid); 1563 1564 $absolute_ttl = idx($params, 'ttl.absolute'); 1565 $relative_ttl = idx($params, 'ttl.relative'); 1566 if ($absolute_ttl !== null && $relative_ttl !== null) { 1567 throw new Exception( 1568 pht( 1569 'Specify an absolute TTL or a relative TTL, but not both.')); 1570 } else if ($absolute_ttl !== null) { 1571 if ($absolute_ttl < PhabricatorTime::getNow()) { 1572 throw new Exception( 1573 pht( 1574 'Absolute TTL must be in the present or future, but TTL "%s" '. 1575 'is in the past.', 1576 $absolute_ttl)); 1577 } 1578 1579 $this->setTtl($absolute_ttl); 1580 } else if ($relative_ttl !== null) { 1581 if ($relative_ttl < 0) { 1582 throw new Exception( 1583 pht( 1584 'Relative TTL must be zero or more seconds, but "%s" is '. 1585 'negative.', 1586 $relative_ttl)); 1587 } 1588 1589 $max_relative = phutil_units('365 days in seconds'); 1590 if ($relative_ttl > $max_relative) { 1591 throw new Exception( 1592 pht( 1593 'Relative TTL must not be more than "%s" seconds, but TTL '. 1594 '"%s" was specified.', 1595 $max_relative, 1596 $relative_ttl)); 1597 } 1598 1599 $absolute_ttl = PhabricatorTime::getNow() + $relative_ttl; 1600 1601 $this->setTtl($absolute_ttl); 1602 } 1603 1604 $view_policy = idx($params, 'viewPolicy'); 1605 if ($view_policy) { 1606 $this->setViewPolicy($params['viewPolicy']); 1607 } 1608 1609 $is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0); 1610 $this->setIsExplicitUpload($is_explicit); 1611 1612 $can_cdn = idx($params, 'canCDN'); 1613 if ($can_cdn) { 1614 $this->setCanCDN(true); 1615 } 1616 1617 $builtin = idx($params, 'builtin'); 1618 if ($builtin) { 1619 $this->setBuiltinName($builtin); 1620 $this->setBuiltinKey($builtin); 1621 } 1622 1623 $profile = idx($params, 'profile'); 1624 if ($profile) { 1625 $this->setIsProfileImage(true); 1626 } 1627 1628 $mime_type = idx($params, 'mime-type'); 1629 if ($mime_type) { 1630 $this->setMimeType($mime_type); 1631 } 1632 1633 $is_chunk = idx($params, 'chunk'); 1634 if ($is_chunk) { 1635 $this->setIsChunk(true); 1636 } 1637 1638 return $this; 1639 } 1640 1641 public function getRedirectResponse() { 1642 $uri = $this->getBestURI(); 1643 1644 // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI 1645 // (if the file is a viewable image) and sometimes a local URI (if not). 1646 // For now, just detect which one we got and configure the response 1647 // appropriately. In the long run, if this endpoint is served from a CDN 1648 // domain, we can't issue a local redirect to an info URI (which is not 1649 // present on the CDN domain). We probably never actually issue local 1650 // redirects here anyway, since we only ever transform viewable images 1651 // right now. 1652 1653 $is_external = strlen(id(new PhutilURI($uri))->getDomain()); 1654 1655 return id(new AphrontRedirectResponse()) 1656 ->setIsExternal($is_external) 1657 ->setURI($uri); 1658 } 1659 1660 public function newDownloadResponse() { 1661 // We're cheating a little bit here and relying on the fact that 1662 // getDownloadURI() always returns a fully qualified URI with a complete 1663 // domain. 1664 return id(new AphrontRedirectResponse()) 1665 ->setIsExternal(true) 1666 ->setCloseDialogBeforeRedirect(true) 1667 ->setURI($this->getDownloadURI()); 1668 } 1669 1670 public function attachTransforms(array $map) { 1671 $this->transforms = $map; 1672 return $this; 1673 } 1674 1675 public function getTransform($key) { 1676 return $this->assertAttachedKey($this->transforms, $key); 1677 } 1678 1679 public function newStorageFormat() { 1680 $key = $this->getStorageFormat(); 1681 $template = PhabricatorFileStorageFormat::requireFormat($key); 1682 1683 $format = id(clone $template) 1684 ->setFile($this); 1685 1686 return $format; 1687 } 1688 1689 1690/* -( PhabricatorApplicationTransactionInterface )------------------------- */ 1691 1692 1693 public function getApplicationTransactionEditor() { 1694 return new PhabricatorFileEditor(); 1695 } 1696 1697 public function getApplicationTransactionTemplate() { 1698 return new PhabricatorFileTransaction(); 1699 } 1700 1701 1702/* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 1703 1704 1705 public function getCapabilities() { 1706 return array( 1707 PhabricatorPolicyCapability::CAN_VIEW, 1708 PhabricatorPolicyCapability::CAN_EDIT, 1709 ); 1710 } 1711 1712 public function getPolicy($capability) { 1713 switch ($capability) { 1714 case PhabricatorPolicyCapability::CAN_VIEW: 1715 if ($this->isBuiltin()) { 1716 return PhabricatorPolicies::getMostOpenPolicy(); 1717 } 1718 if ($this->getIsProfileImage()) { 1719 return PhabricatorPolicies::getMostOpenPolicy(); 1720 } 1721 return $this->getViewPolicy(); 1722 case PhabricatorPolicyCapability::CAN_EDIT: 1723 return PhabricatorPolicies::POLICY_NOONE; 1724 } 1725 } 1726 1727 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 1728 $viewer_phid = $viewer->getPHID(); 1729 if ($viewer_phid) { 1730 if ($this->getAuthorPHID() == $viewer_phid) { 1731 return true; 1732 } 1733 } 1734 1735 switch ($capability) { 1736 case PhabricatorPolicyCapability::CAN_VIEW: 1737 // If you can see the file this file is a transform of, you can see 1738 // this file. 1739 if ($this->getOriginalFile()) { 1740 return true; 1741 } 1742 1743 // If you can see any object this file is attached to, you can see 1744 // the file. 1745 return (count($this->getObjects()) > 0); 1746 } 1747 1748 return false; 1749 } 1750 1751 public function describeAutomaticCapability($capability) { 1752 $out = array(); 1753 $out[] = pht('The user who uploaded a file can always view and edit it.'); 1754 switch ($capability) { 1755 case PhabricatorPolicyCapability::CAN_VIEW: 1756 $out[] = pht( 1757 'Files attached to objects are visible to users who can view '. 1758 'those objects.'); 1759 $out[] = pht( 1760 'Thumbnails are visible only to users who can view the original '. 1761 'file.'); 1762 break; 1763 } 1764 1765 return $out; 1766 } 1767 1768 1769/* -( PhabricatorSubscribableInterface Implementation )-------------------- */ 1770 1771 1772 public function isAutomaticallySubscribed($phid) { 1773 return ($this->authorPHID == $phid); 1774 } 1775 1776 1777/* -( PhabricatorTokenReceiverInterface )---------------------------------- */ 1778 1779 1780 public function getUsersToNotifyOfTokenGiven() { 1781 return array( 1782 $this->getAuthorPHID(), 1783 ); 1784 } 1785 1786 1787/* -( PhabricatorDestructibleInterface )----------------------------------- */ 1788 1789 1790 public function destroyObjectPermanently( 1791 PhabricatorDestructionEngine $engine) { 1792 1793 $this->openTransaction(); 1794 1795 $attachments = id(new PhabricatorFileAttachment())->loadAllWhere( 1796 'filePHID = %s', 1797 $this->getPHID()); 1798 foreach ($attachments as $attachment) { 1799 $attachment->delete(); 1800 } 1801 1802 $this->delete(); 1803 $this->saveTransaction(); 1804 } 1805 1806 1807/* -( PhabricatorConduitResultInterface )---------------------------------- */ 1808 1809 1810 public function getFieldSpecificationsForConduit() { 1811 return array( 1812 id(new PhabricatorConduitSearchFieldSpecification()) 1813 ->setKey('name') 1814 ->setType('string') 1815 ->setDescription(pht('The name of the file.')), 1816 id(new PhabricatorConduitSearchFieldSpecification()) 1817 ->setKey('uri') 1818 ->setType('uri') 1819 ->setDescription(pht('View URI for the file.')), 1820 id(new PhabricatorConduitSearchFieldSpecification()) 1821 ->setKey('dataURI') 1822 ->setType('uri') 1823 ->setDescription(pht('Download URI for the file data.')), 1824 id(new PhabricatorConduitSearchFieldSpecification()) 1825 ->setKey('size') 1826 ->setType('int') 1827 ->setDescription(pht('File size, in bytes.')), 1828 ); 1829 } 1830 1831 public function getFieldValuesForConduit() { 1832 return array( 1833 'name' => $this->getName(), 1834 'uri' => PhabricatorEnv::getURI($this->getURI()), 1835 'dataURI' => $this->getCDNURI('data'), 1836 'size' => (int)$this->getByteSize(), 1837 'alt' => array( 1838 'custom' => $this->getCustomAltText(), 1839 'default' => $this->getDefaultAltText(), 1840 ), 1841 ); 1842 } 1843 1844 public function getConduitSearchAttachments() { 1845 return array(); 1846 } 1847 1848/* -( PhabricatorNgramInterface )------------------------------------------ */ 1849 1850 1851 public function newNgrams() { 1852 return array( 1853 id(new PhabricatorFileNameNgrams()) 1854 ->setValue($this->getName()), 1855 ); 1856 } 1857 1858}