[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 | Temporary file lifetime, in seconds. 13 * | viewPolicy | File visibility policy. 14 * | isExplicitUpload | Used to show users files they explicitly uploaded. 15 * | canCDN | Allows the file to be cached and delivered over a CDN. 16 * | mime-type | Optional, explicit file MIME type. 17 * 18 */ 19 final class PhabricatorFile extends PhabricatorFileDAO 20 implements 21 PhabricatorTokenReceiverInterface, 22 PhabricatorSubscribableInterface, 23 PhabricatorFlaggableInterface, 24 PhabricatorPolicyInterface, 25 PhabricatorDestructibleInterface { 26 27 const ONETIME_TEMPORARY_TOKEN_TYPE = 'file:onetime'; 28 const STORAGE_FORMAT_RAW = 'raw'; 29 30 const METADATA_IMAGE_WIDTH = 'width'; 31 const METADATA_IMAGE_HEIGHT = 'height'; 32 const METADATA_CAN_CDN = 'canCDN'; 33 34 protected $name; 35 protected $mimeType; 36 protected $byteSize; 37 protected $authorPHID; 38 protected $secretKey; 39 protected $contentHash; 40 protected $metadata = array(); 41 protected $mailKey; 42 43 protected $storageEngine; 44 protected $storageFormat; 45 protected $storageHandle; 46 47 protected $ttl; 48 protected $isExplicitUpload = 1; 49 protected $viewPolicy = PhabricatorPolicies::POLICY_USER; 50 51 private $objects = self::ATTACHABLE; 52 private $objectPHIDs = self::ATTACHABLE; 53 private $originalFile = self::ATTACHABLE; 54 55 public static function initializeNewFile() { 56 $app = id(new PhabricatorApplicationQuery()) 57 ->setViewer(PhabricatorUser::getOmnipotentUser()) 58 ->withClasses(array('PhabricatorFilesApplication')) 59 ->executeOne(); 60 61 $view_policy = $app->getPolicy( 62 FilesDefaultViewCapability::CAPABILITY); 63 64 return id(new PhabricatorFile()) 65 ->setViewPolicy($view_policy) 66 ->attachOriginalFile(null) 67 ->attachObjects(array()) 68 ->attachObjectPHIDs(array()); 69 } 70 71 public function getConfiguration() { 72 return array( 73 self::CONFIG_AUX_PHID => true, 74 self::CONFIG_SERIALIZATION => array( 75 'metadata' => self::SERIALIZATION_JSON, 76 ), 77 self::CONFIG_COLUMN_SCHEMA => array( 78 'name' => 'text255?', 79 'mimeType' => 'text255?', 80 'byteSize' => 'uint64', 81 'storageEngine' => 'text32', 82 'storageFormat' => 'text32', 83 'storageHandle' => 'text255', 84 'authorPHID' => 'phid?', 85 'secretKey' => 'bytes20?', 86 'contentHash' => 'bytes40?', 87 'ttl' => 'epoch?', 88 'isExplicitUpload' => 'bool?', 89 'mailKey' => 'bytes20', 90 ), 91 self::CONFIG_KEY_SCHEMA => array( 92 'key_phid' => null, 93 'phid' => array( 94 'columns' => array('phid'), 95 'unique' => true, 96 ), 97 'authorPHID' => array( 98 'columns' => array('authorPHID'), 99 ), 100 'contentHash' => array( 101 'columns' => array('contentHash'), 102 ), 103 'key_ttl' => array( 104 'columns' => array('ttl'), 105 ), 106 'key_dateCreated' => array( 107 'columns' => array('dateCreated'), 108 ), 109 ), 110 ) + parent::getConfiguration(); 111 } 112 113 public function generatePHID() { 114 return PhabricatorPHID::generateNewPHID( 115 PhabricatorFileFilePHIDType::TYPECONST); 116 } 117 118 public function save() { 119 if (!$this->getSecretKey()) { 120 $this->setSecretKey($this->generateSecretKey()); 121 } 122 if (!$this->getMailKey()) { 123 $this->setMailKey(Filesystem::readRandomCharacters(20)); 124 } 125 return parent::save(); 126 } 127 128 public function getMonogram() { 129 return 'F'.$this->getID(); 130 } 131 132 public static function readUploadedFileData($spec) { 133 if (!$spec) { 134 throw new Exception('No file was uploaded!'); 135 } 136 137 $err = idx($spec, 'error'); 138 if ($err) { 139 throw new PhabricatorFileUploadException($err); 140 } 141 142 $tmp_name = idx($spec, 'tmp_name'); 143 $is_valid = @is_uploaded_file($tmp_name); 144 if (!$is_valid) { 145 throw new Exception('File is not an uploaded file.'); 146 } 147 148 $file_data = Filesystem::readFile($tmp_name); 149 $file_size = idx($spec, 'size'); 150 151 if (strlen($file_data) != $file_size) { 152 throw new Exception('File size disagrees with uploaded size.'); 153 } 154 155 self::validateFileSize(strlen($file_data)); 156 157 return $file_data; 158 } 159 160 public static function newFromPHPUpload($spec, array $params = array()) { 161 $file_data = self::readUploadedFileData($spec); 162 163 $file_name = nonempty( 164 idx($params, 'name'), 165 idx($spec, 'name')); 166 $params = array( 167 'name' => $file_name, 168 ) + $params; 169 170 return self::newFromFileData($file_data, $params); 171 } 172 173 public static function newFromXHRUpload($data, array $params = array()) { 174 self::validateFileSize(strlen($data)); 175 return self::newFromFileData($data, $params); 176 } 177 178 private static function validateFileSize($size) { 179 $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); 180 if (!$limit) { 181 return; 182 } 183 184 $limit = phutil_parse_bytes($limit); 185 if ($size > $limit) { 186 throw new PhabricatorFileUploadException(-1000); 187 } 188 } 189 190 191 /** 192 * Given a block of data, try to load an existing file with the same content 193 * if one exists. If it does not, build a new file. 194 * 195 * This method is generally used when we have some piece of semi-trusted data 196 * like a diff or a file from a repository that we want to show to the user. 197 * We can't just dump it out because it may be dangerous for any number of 198 * reasons; instead, we need to serve it through the File abstraction so it 199 * ends up on the CDN domain if one is configured and so on. However, if we 200 * simply wrote a new file every time we'd potentially end up with a lot 201 * of redundant data in file storage. 202 * 203 * To solve these problems, we use file storage as a cache and reuse the 204 * same file again if we've previously written it. 205 * 206 * NOTE: This method unguards writes. 207 * 208 * @param string Raw file data. 209 * @param dict Dictionary of file information. 210 */ 211 public static function buildFromFileDataOrHash( 212 $data, 213 array $params = array()) { 214 215 $file = id(new PhabricatorFile())->loadOneWhere( 216 'name = %s AND contentHash = %s LIMIT 1', 217 self::normalizeFileName(idx($params, 'name')), 218 self::hashFileContent($data)); 219 220 if (!$file) { 221 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 222 $file = PhabricatorFile::newFromFileData($data, $params); 223 unset($unguarded); 224 } 225 226 return $file; 227 } 228 229 public static function newFileFromContentHash($hash, array $params) { 230 // Check to see if a file with same contentHash exist 231 $file = id(new PhabricatorFile())->loadOneWhere( 232 'contentHash = %s LIMIT 1', 233 $hash); 234 235 if ($file) { 236 // copy storageEngine, storageHandle, storageFormat 237 $copy_of_storage_engine = $file->getStorageEngine(); 238 $copy_of_storage_handle = $file->getStorageHandle(); 239 $copy_of_storage_format = $file->getStorageFormat(); 240 $copy_of_byteSize = $file->getByteSize(); 241 $copy_of_mimeType = $file->getMimeType(); 242 243 $new_file = PhabricatorFile::initializeNewFile(); 244 245 $new_file->setByteSize($copy_of_byteSize); 246 247 $new_file->setContentHash($hash); 248 $new_file->setStorageEngine($copy_of_storage_engine); 249 $new_file->setStorageHandle($copy_of_storage_handle); 250 $new_file->setStorageFormat($copy_of_storage_format); 251 $new_file->setMimeType($copy_of_mimeType); 252 $new_file->copyDimensions($file); 253 254 $new_file->readPropertiesFromParameters($params); 255 256 $new_file->save(); 257 258 return $new_file; 259 } 260 261 return $file; 262 } 263 264 private static function buildFromFileData($data, array $params = array()) { 265 266 if (isset($params['storageEngines'])) { 267 $engines = $params['storageEngines']; 268 } else { 269 $selector = PhabricatorEnv::newObjectFromConfig( 270 'storage.engine-selector'); 271 $engines = $selector->selectStorageEngines($data, $params); 272 } 273 274 assert_instances_of($engines, 'PhabricatorFileStorageEngine'); 275 if (!$engines) { 276 throw new Exception('No valid storage engines are available!'); 277 } 278 279 $file = PhabricatorFile::initializeNewFile(); 280 281 $data_handle = null; 282 $engine_identifier = null; 283 $exceptions = array(); 284 foreach ($engines as $engine) { 285 $engine_class = get_class($engine); 286 try { 287 list($engine_identifier, $data_handle) = $file->writeToEngine( 288 $engine, 289 $data, 290 $params); 291 292 // We stored the file somewhere so stop trying to write it to other 293 // places. 294 break; 295 } catch (PhabricatorFileStorageConfigurationException $ex) { 296 // If an engine is outright misconfigured (or misimplemented), raise 297 // that immediately since it probably needs attention. 298 throw $ex; 299 } catch (Exception $ex) { 300 phlog($ex); 301 302 // If an engine doesn't work, keep trying all the other valid engines 303 // in case something else works. 304 $exceptions[$engine_class] = $ex; 305 } 306 } 307 308 if (!$data_handle) { 309 throw new PhutilAggregateException( 310 'All storage engines failed to write file:', 311 $exceptions); 312 } 313 314 $file->setByteSize(strlen($data)); 315 $file->setContentHash(self::hashFileContent($data)); 316 317 $file->setStorageEngine($engine_identifier); 318 $file->setStorageHandle($data_handle); 319 320 // TODO: This is probably YAGNI, but allows for us to do encryption or 321 // compression later if we want. 322 $file->setStorageFormat(self::STORAGE_FORMAT_RAW); 323 324 $file->readPropertiesFromParameters($params); 325 326 if (!$file->getMimeType()) { 327 $tmp = new TempFile(); 328 Filesystem::writeFile($tmp, $data); 329 $file->setMimeType(Filesystem::getMimeType($tmp)); 330 } 331 332 try { 333 $file->updateDimensions(false); 334 } catch (Exception $ex) { 335 // Do nothing 336 } 337 338 $file->save(); 339 340 return $file; 341 } 342 343 public static function newFromFileData($data, array $params = array()) { 344 $hash = self::hashFileContent($data); 345 $file = self::newFileFromContentHash($hash, $params); 346 347 if ($file) { 348 return $file; 349 } 350 351 return self::buildFromFileData($data, $params); 352 } 353 354 public function migrateToEngine(PhabricatorFileStorageEngine $engine) { 355 if (!$this->getID() || !$this->getStorageHandle()) { 356 throw new Exception( 357 "You can not migrate a file which hasn't yet been saved."); 358 } 359 360 $data = $this->loadFileData(); 361 $params = array( 362 'name' => $this->getName(), 363 ); 364 365 list($new_identifier, $new_handle) = $this->writeToEngine( 366 $engine, 367 $data, 368 $params); 369 370 $old_engine = $this->instantiateStorageEngine(); 371 $old_identifier = $this->getStorageEngine(); 372 $old_handle = $this->getStorageHandle(); 373 374 $this->setStorageEngine($new_identifier); 375 $this->setStorageHandle($new_handle); 376 $this->save(); 377 378 $this->deleteFileDataIfUnused( 379 $old_engine, 380 $old_identifier, 381 $old_handle); 382 383 return $this; 384 } 385 386 private function writeToEngine( 387 PhabricatorFileStorageEngine $engine, 388 $data, 389 array $params) { 390 391 $engine_class = get_class($engine); 392 393 $data_handle = $engine->writeFile($data, $params); 394 395 if (!$data_handle || strlen($data_handle) > 255) { 396 // This indicates an improperly implemented storage engine. 397 throw new PhabricatorFileStorageConfigurationException( 398 "Storage engine '{$engine_class}' executed writeFile() but did ". 399 "not return a valid handle ('{$data_handle}') to the data: it ". 400 "must be nonempty and no longer than 255 characters."); 401 } 402 403 $engine_identifier = $engine->getEngineIdentifier(); 404 if (!$engine_identifier || strlen($engine_identifier) > 32) { 405 throw new PhabricatorFileStorageConfigurationException( 406 "Storage engine '{$engine_class}' returned an improper engine ". 407 "identifier '{$engine_identifier}': it must be nonempty ". 408 "and no longer than 32 characters."); 409 } 410 411 return array($engine_identifier, $data_handle); 412 } 413 414 415 public static function newFromFileDownload($uri, array $params = array()) { 416 // Make sure we're allowed to make a request first 417 if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) { 418 throw new Exception('Outbound HTTP requests are disabled!'); 419 } 420 421 $uri = new PhutilURI($uri); 422 423 $protocol = $uri->getProtocol(); 424 switch ($protocol) { 425 case 'http': 426 case 'https': 427 break; 428 default: 429 // Make sure we are not accessing any file:// URIs or similar. 430 return null; 431 } 432 433 $timeout = 5; 434 435 list($file_data) = id(new HTTPSFuture($uri)) 436 ->setTimeout($timeout) 437 ->resolvex(); 438 439 $params = $params + array( 440 'name' => basename($uri), 441 ); 442 443 return self::newFromFileData($file_data, $params); 444 } 445 446 public static function normalizeFileName($file_name) { 447 $pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@"; 448 $file_name = preg_replace($pattern, '_', $file_name); 449 $file_name = preg_replace('@_+@', '_', $file_name); 450 $file_name = trim($file_name, '_'); 451 452 $disallowed_filenames = array( 453 '.' => 'dot', 454 '..' => 'dotdot', 455 '' => 'file', 456 ); 457 $file_name = idx($disallowed_filenames, $file_name, $file_name); 458 459 return $file_name; 460 } 461 462 public function delete() { 463 // We want to delete all the rows which mark this file as the transformation 464 // of some other file (since we're getting rid of it). We also delete all 465 // the transformations of this file, so that a user who deletes an image 466 // doesn't need to separately hunt down and delete a bunch of thumbnails and 467 // resizes of it. 468 469 $outbound_xforms = id(new PhabricatorFileQuery()) 470 ->setViewer(PhabricatorUser::getOmnipotentUser()) 471 ->withTransforms( 472 array( 473 array( 474 'originalPHID' => $this->getPHID(), 475 'transform' => true, 476 ), 477 )) 478 ->execute(); 479 480 foreach ($outbound_xforms as $outbound_xform) { 481 $outbound_xform->delete(); 482 } 483 484 $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 485 'transformedPHID = %s', 486 $this->getPHID()); 487 488 $this->openTransaction(); 489 foreach ($inbound_xforms as $inbound_xform) { 490 $inbound_xform->delete(); 491 } 492 $ret = parent::delete(); 493 $this->saveTransaction(); 494 495 $this->deleteFileDataIfUnused( 496 $this->instantiateStorageEngine(), 497 $this->getStorageEngine(), 498 $this->getStorageHandle()); 499 500 return $ret; 501 } 502 503 504 /** 505 * Destroy stored file data if there are no remaining files which reference 506 * it. 507 */ 508 public function deleteFileDataIfUnused( 509 PhabricatorFileStorageEngine $engine, 510 $engine_identifier, 511 $handle) { 512 513 // Check to see if any files are using storage. 514 $usage = id(new PhabricatorFile())->loadAllWhere( 515 'storageEngine = %s AND storageHandle = %s LIMIT 1', 516 $engine_identifier, 517 $handle); 518 519 // If there are no files using the storage, destroy the actual storage. 520 if (!$usage) { 521 try { 522 $engine->deleteFile($handle); 523 } catch (Exception $ex) { 524 // In the worst case, we're leaving some data stranded in a storage 525 // engine, which is not a big deal. 526 phlog($ex); 527 } 528 } 529 } 530 531 532 public static function hashFileContent($data) { 533 return sha1($data); 534 } 535 536 public function loadFileData() { 537 538 $engine = $this->instantiateStorageEngine(); 539 $data = $engine->readFile($this->getStorageHandle()); 540 541 switch ($this->getStorageFormat()) { 542 case self::STORAGE_FORMAT_RAW: 543 $data = $data; 544 break; 545 default: 546 throw new Exception('Unknown storage format.'); 547 } 548 549 return $data; 550 } 551 552 public function getViewURI() { 553 if (!$this->getPHID()) { 554 throw new Exception( 555 'You must save a file before you can generate a view URI.'); 556 } 557 558 $name = phutil_escape_uri($this->getName()); 559 560 $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name; 561 return PhabricatorEnv::getCDNURI($path); 562 } 563 564 public function getInfoURI() { 565 return '/'.$this->getMonogram(); 566 } 567 568 public function getBestURI() { 569 if ($this->isViewableInBrowser()) { 570 return $this->getViewURI(); 571 } else { 572 return $this->getInfoURI(); 573 } 574 } 575 576 public function getDownloadURI() { 577 $uri = id(new PhutilURI($this->getViewURI())) 578 ->setQueryParam('download', true); 579 return (string) $uri; 580 } 581 582 public function getProfileThumbURI() { 583 $path = '/file/xform/thumb-profile/'.$this->getPHID().'/' 584 .$this->getSecretKey().'/'; 585 return PhabricatorEnv::getCDNURI($path); 586 } 587 588 public function getThumb60x45URI() { 589 $path = '/file/xform/thumb-60x45/'.$this->getPHID().'/' 590 .$this->getSecretKey().'/'; 591 return PhabricatorEnv::getCDNURI($path); 592 } 593 594 public function getThumb160x120URI() { 595 $path = '/file/xform/thumb-160x120/'.$this->getPHID().'/' 596 .$this->getSecretKey().'/'; 597 return PhabricatorEnv::getCDNURI($path); 598 } 599 600 public function getPreview100URI() { 601 $path = '/file/xform/preview-100/'.$this->getPHID().'/' 602 .$this->getSecretKey().'/'; 603 return PhabricatorEnv::getCDNURI($path); 604 } 605 606 public function getPreview220URI() { 607 $path = '/file/xform/preview-220/'.$this->getPHID().'/' 608 .$this->getSecretKey().'/'; 609 return PhabricatorEnv::getCDNURI($path); 610 } 611 612 public function getThumb220x165URI() { 613 $path = '/file/xform/thumb-220x165/'.$this->getPHID().'/' 614 .$this->getSecretKey().'/'; 615 return PhabricatorEnv::getCDNURI($path); 616 } 617 618 public function getThumb280x210URI() { 619 $path = '/file/xform/thumb-280x210/'.$this->getPHID().'/' 620 .$this->getSecretKey().'/'; 621 return PhabricatorEnv::getCDNURI($path); 622 } 623 624 public function isViewableInBrowser() { 625 return ($this->getViewableMimeType() !== null); 626 } 627 628 public function isViewableImage() { 629 if (!$this->isViewableInBrowser()) { 630 return false; 631 } 632 633 $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); 634 $mime_type = $this->getMimeType(); 635 return idx($mime_map, $mime_type); 636 } 637 638 public function isAudio() { 639 if (!$this->isViewableInBrowser()) { 640 return false; 641 } 642 643 $mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); 644 $mime_type = $this->getMimeType(); 645 return idx($mime_map, $mime_type); 646 } 647 648 public function isTransformableImage() { 649 // NOTE: The way the 'gd' extension works in PHP is that you can install it 650 // with support for only some file types, so it might be able to handle 651 // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup 652 // warns you if you don't have complete support. 653 654 $matches = null; 655 $ok = preg_match( 656 '@^image/(gif|png|jpe?g)@', 657 $this->getViewableMimeType(), 658 $matches); 659 if (!$ok) { 660 return false; 661 } 662 663 switch ($matches[1]) { 664 case 'jpg'; 665 case 'jpeg': 666 return function_exists('imagejpeg'); 667 break; 668 case 'png': 669 return function_exists('imagepng'); 670 break; 671 case 'gif': 672 return function_exists('imagegif'); 673 break; 674 default: 675 throw new Exception('Unknown type matched as image MIME type.'); 676 } 677 } 678 679 public static function getTransformableImageFormats() { 680 $supported = array(); 681 682 if (function_exists('imagejpeg')) { 683 $supported[] = 'jpg'; 684 } 685 686 if (function_exists('imagepng')) { 687 $supported[] = 'png'; 688 } 689 690 if (function_exists('imagegif')) { 691 $supported[] = 'gif'; 692 } 693 694 return $supported; 695 } 696 697 public function instantiateStorageEngine() { 698 return self::buildEngine($this->getStorageEngine()); 699 } 700 701 public static function buildEngine($engine_identifier) { 702 $engines = self::buildAllEngines(); 703 foreach ($engines as $engine) { 704 if ($engine->getEngineIdentifier() == $engine_identifier) { 705 return $engine; 706 } 707 } 708 709 throw new Exception( 710 "Storage engine '{$engine_identifier}' could not be located!"); 711 } 712 713 public static function buildAllEngines() { 714 $engines = id(new PhutilSymbolLoader()) 715 ->setType('class') 716 ->setConcreteOnly(true) 717 ->setAncestorClass('PhabricatorFileStorageEngine') 718 ->selectAndLoadSymbols(); 719 720 $results = array(); 721 foreach ($engines as $engine_class) { 722 $results[] = newv($engine_class['name'], array()); 723 } 724 725 return $results; 726 } 727 728 public function getViewableMimeType() { 729 $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); 730 731 $mime_type = $this->getMimeType(); 732 $mime_parts = explode(';', $mime_type); 733 $mime_type = trim(reset($mime_parts)); 734 735 return idx($mime_map, $mime_type); 736 } 737 738 public function getDisplayIconForMimeType() { 739 $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types'); 740 $mime_type = $this->getMimeType(); 741 return idx($mime_map, $mime_type, 'docs_file'); 742 } 743 744 public function validateSecretKey($key) { 745 return ($key == $this->getSecretKey()); 746 } 747 748 public function generateSecretKey() { 749 return Filesystem::readRandomCharacters(20); 750 } 751 752 public function updateDimensions($save = true) { 753 if (!$this->isViewableImage()) { 754 throw new Exception( 755 'This file is not a viewable image.'); 756 } 757 758 if (!function_exists('imagecreatefromstring')) { 759 throw new Exception( 760 'Cannot retrieve image information.'); 761 } 762 763 $data = $this->loadFileData(); 764 765 $img = imagecreatefromstring($data); 766 if ($img === false) { 767 throw new Exception( 768 'Error when decoding image.'); 769 } 770 771 $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); 772 $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); 773 774 if ($save) { 775 $this->save(); 776 } 777 778 return $this; 779 } 780 781 public function copyDimensions(PhabricatorFile $file) { 782 $metadata = $file->getMetadata(); 783 $width = idx($metadata, self::METADATA_IMAGE_WIDTH); 784 if ($width) { 785 $this->metadata[self::METADATA_IMAGE_WIDTH] = $width; 786 } 787 $height = idx($metadata, self::METADATA_IMAGE_HEIGHT); 788 if ($height) { 789 $this->metadata[self::METADATA_IMAGE_HEIGHT] = $height; 790 } 791 792 return $this; 793 } 794 795 796 /** 797 * Load (or build) the {@class:PhabricatorFile} objects for builtin file 798 * resources. The builtin mechanism allows files shipped with Phabricator 799 * to be treated like normal files so that APIs do not need to special case 800 * things like default images or deleted files. 801 * 802 * Builtins are located in `resources/builtin/` and identified by their 803 * name. 804 * 805 * @param PhabricatorUser Viewing user. 806 * @param list<string> List of builtin file names. 807 * @return dict<string, PhabricatorFile> Dictionary of named builtins. 808 */ 809 public static function loadBuiltins(PhabricatorUser $user, array $names) { 810 $specs = array(); 811 foreach ($names as $name) { 812 $specs[] = array( 813 'originalPHID' => PhabricatorPHIDConstants::PHID_VOID, 814 'transform' => 'builtin:'.$name, 815 ); 816 } 817 818 // NOTE: Anyone is allowed to access builtin files. 819 820 $files = id(new PhabricatorFileQuery()) 821 ->setViewer(PhabricatorUser::getOmnipotentUser()) 822 ->withTransforms($specs) 823 ->execute(); 824 825 $files = mpull($files, null, 'getName'); 826 827 $root = dirname(phutil_get_library_root('phabricator')); 828 $root = $root.'/resources/builtin/'; 829 830 $build = array(); 831 foreach ($names as $name) { 832 if (isset($files[$name])) { 833 continue; 834 } 835 836 // This is just a sanity check to prevent loading arbitrary files. 837 if (basename($name) != $name) { 838 throw new Exception("Invalid builtin name '{$name}'!"); 839 } 840 841 $path = $root.$name; 842 843 if (!Filesystem::pathExists($path)) { 844 throw new Exception("Builtin '{$path}' does not exist!"); 845 } 846 847 $data = Filesystem::readFile($path); 848 $params = array( 849 'name' => $name, 850 'ttl' => time() + (60 * 60 * 24 * 7), 851 ); 852 853 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 854 $file = PhabricatorFile::newFromFileData($data, $params); 855 $xform = id(new PhabricatorTransformedFile()) 856 ->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID) 857 ->setTransform('builtin:'.$name) 858 ->setTransformedPHID($file->getPHID()) 859 ->save(); 860 unset($unguarded); 861 862 $file->attachObjectPHIDs(array()); 863 $file->attachObjects(array()); 864 865 $files[$name] = $file; 866 } 867 868 return $files; 869 } 870 871 872 /** 873 * Convenience wrapper for @{method:loadBuiltins}. 874 * 875 * @param PhabricatorUser Viewing user. 876 * @param string Single builtin name to load. 877 * @return PhabricatorFile Corresponding builtin file. 878 */ 879 public static function loadBuiltin(PhabricatorUser $user, $name) { 880 return idx(self::loadBuiltins($user, array($name)), $name); 881 } 882 883 public function getObjects() { 884 return $this->assertAttached($this->objects); 885 } 886 887 public function attachObjects(array $objects) { 888 $this->objects = $objects; 889 return $this; 890 } 891 892 public function getObjectPHIDs() { 893 return $this->assertAttached($this->objectPHIDs); 894 } 895 896 public function attachObjectPHIDs(array $object_phids) { 897 $this->objectPHIDs = $object_phids; 898 return $this; 899 } 900 901 public function getOriginalFile() { 902 return $this->assertAttached($this->originalFile); 903 } 904 905 public function attachOriginalFile(PhabricatorFile $file = null) { 906 $this->originalFile = $file; 907 return $this; 908 } 909 910 public function getImageHeight() { 911 if (!$this->isViewableImage()) { 912 return null; 913 } 914 return idx($this->metadata, self::METADATA_IMAGE_HEIGHT); 915 } 916 917 public function getImageWidth() { 918 if (!$this->isViewableImage()) { 919 return null; 920 } 921 return idx($this->metadata, self::METADATA_IMAGE_WIDTH); 922 } 923 924 public function getCanCDN() { 925 if (!$this->isViewableImage()) { 926 return false; 927 } 928 929 return idx($this->metadata, self::METADATA_CAN_CDN); 930 } 931 932 public function setCanCDN($can_cdn) { 933 $this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0; 934 return $this; 935 } 936 937 protected function generateOneTimeToken() { 938 $key = Filesystem::readRandomCharacters(16); 939 940 // Save the new secret. 941 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 942 $token = id(new PhabricatorAuthTemporaryToken()) 943 ->setObjectPHID($this->getPHID()) 944 ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE) 945 ->setTokenExpires(time() + phutil_units('1 hour in seconds')) 946 ->setTokenCode(PhabricatorHash::digest($key)) 947 ->save(); 948 unset($unguarded); 949 950 return $key; 951 } 952 953 public function validateOneTimeToken($token_code) { 954 $token = id(new PhabricatorAuthTemporaryTokenQuery()) 955 ->setViewer(PhabricatorUser::getOmnipotentUser()) 956 ->withObjectPHIDs(array($this->getPHID())) 957 ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE)) 958 ->withExpired(false) 959 ->withTokenCodes(array(PhabricatorHash::digest($token_code))) 960 ->executeOne(); 961 962 return $token; 963 } 964 965 /** Get the CDN uri for this file 966 * This will generate a one-time-use token if 967 * security.alternate_file_domain is set in the config. 968 */ 969 public function getCDNURIWithToken() { 970 if (!$this->getPHID()) { 971 throw new Exception( 972 'You must save a file before you can generate a CDN URI.'); 973 } 974 $name = phutil_escape_uri($this->getName()); 975 976 $path = '/file/data' 977 .'/'.$this->getSecretKey() 978 .'/'.$this->getPHID() 979 .'/'.$this->generateOneTimeToken() 980 .'/'.$name; 981 return PhabricatorEnv::getCDNURI($path); 982 } 983 984 985 986 /** 987 * Write the policy edge between this file and some object. 988 * 989 * @param phid Object PHID to attach to. 990 * @return this 991 */ 992 public function attachToObject($phid) { 993 $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; 994 995 id(new PhabricatorEdgeEditor()) 996 ->addEdge($phid, $edge_type, $this->getPHID()) 997 ->save(); 998 999 return $this; 1000 } 1001 1002 1003 /** 1004 * Remove the policy edge between this file and some object. 1005 * 1006 * @param phid Object PHID to detach from. 1007 * @return this 1008 */ 1009 public function detachFromObject($phid) { 1010 $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; 1011 1012 id(new PhabricatorEdgeEditor()) 1013 ->removeEdge($phid, $edge_type, $this->getPHID()) 1014 ->save(); 1015 1016 return $this; 1017 } 1018 1019 1020 /** 1021 * Configure a newly created file object according to specified parameters. 1022 * 1023 * This method is called both when creating a file from fresh data, and 1024 * when creating a new file which reuses existing storage. 1025 * 1026 * @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile} 1027 * for documentation. 1028 * @return this 1029 */ 1030 private function readPropertiesFromParameters(array $params) { 1031 $file_name = idx($params, 'name'); 1032 $file_name = self::normalizeFileName($file_name); 1033 $this->setName($file_name); 1034 1035 $author_phid = idx($params, 'authorPHID'); 1036 $this->setAuthorPHID($author_phid); 1037 1038 $file_ttl = idx($params, 'ttl'); 1039 $this->setTtl($file_ttl); 1040 1041 $view_policy = idx($params, 'viewPolicy'); 1042 if ($view_policy) { 1043 $this->setViewPolicy($params['viewPolicy']); 1044 } 1045 1046 $is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0); 1047 $this->setIsExplicitUpload($is_explicit); 1048 1049 $can_cdn = idx($params, 'canCDN'); 1050 if ($can_cdn) { 1051 $this->setCanCDN(true); 1052 } 1053 1054 $mime_type = idx($params, 'mime-type'); 1055 if ($mime_type) { 1056 $this->setMimeType($mime_type); 1057 } 1058 1059 return $this; 1060 } 1061 1062 public function getRedirectResponse() { 1063 $uri = $this->getBestURI(); 1064 1065 // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI 1066 // (if the file is a viewable image) and sometimes a local URI (if not). 1067 // For now, just detect which one we got and configure the response 1068 // appropriately. In the long run, if this endpoint is served from a CDN 1069 // domain, we can't issue a local redirect to an info URI (which is not 1070 // present on the CDN domain). We probably never actually issue local 1071 // redirects here anyway, since we only ever transform viewable images 1072 // right now. 1073 1074 $is_external = strlen(id(new PhutilURI($uri))->getDomain()); 1075 1076 return id(new AphrontRedirectResponse()) 1077 ->setIsExternal($is_external) 1078 ->setURI($uri); 1079 } 1080 1081 1082 /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ 1083 1084 1085 public function getCapabilities() { 1086 return array( 1087 PhabricatorPolicyCapability::CAN_VIEW, 1088 PhabricatorPolicyCapability::CAN_EDIT, 1089 ); 1090 } 1091 1092 public function getPolicy($capability) { 1093 switch ($capability) { 1094 case PhabricatorPolicyCapability::CAN_VIEW: 1095 return $this->getViewPolicy(); 1096 case PhabricatorPolicyCapability::CAN_EDIT: 1097 return PhabricatorPolicies::POLICY_NOONE; 1098 } 1099 } 1100 1101 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 1102 $viewer_phid = $viewer->getPHID(); 1103 if ($viewer_phid) { 1104 if ($this->getAuthorPHID() == $viewer_phid) { 1105 return true; 1106 } 1107 } 1108 1109 switch ($capability) { 1110 case PhabricatorPolicyCapability::CAN_VIEW: 1111 // If you can see the file this file is a transform of, you can see 1112 // this file. 1113 if ($this->getOriginalFile()) { 1114 return true; 1115 } 1116 1117 // If you can see any object this file is attached to, you can see 1118 // the file. 1119 return (count($this->getObjects()) > 0); 1120 } 1121 1122 return false; 1123 } 1124 1125 public function describeAutomaticCapability($capability) { 1126 $out = array(); 1127 $out[] = pht('The user who uploaded a file can always view and edit it.'); 1128 switch ($capability) { 1129 case PhabricatorPolicyCapability::CAN_VIEW: 1130 $out[] = pht( 1131 'Files attached to objects are visible to users who can view '. 1132 'those objects.'); 1133 $out[] = pht( 1134 'Thumbnails are visible only to users who can view the original '. 1135 'file.'); 1136 break; 1137 } 1138 1139 return $out; 1140 } 1141 1142 1143 /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ 1144 1145 1146 public function isAutomaticallySubscribed($phid) { 1147 return ($this->authorPHID == $phid); 1148 } 1149 1150 public function shouldShowSubscribersProperty() { 1151 return true; 1152 } 1153 1154 public function shouldAllowSubscription($phid) { 1155 return true; 1156 } 1157 1158 1159 /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ 1160 1161 1162 public function getUsersToNotifyOfTokenGiven() { 1163 return array( 1164 $this->getAuthorPHID(), 1165 ); 1166 } 1167 1168 1169 /* -( PhabricatorDestructibleInterface )----------------------------------- */ 1170 1171 1172 public function destroyObjectPermanently( 1173 PhabricatorDestructionEngine $engine) { 1174 1175 $this->openTransaction(); 1176 $this->delete(); 1177 $this->saveTransaction(); 1178 } 1179 1180 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |