[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/files/storage/ -> PhabricatorFile.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1