2011-01-23 03:33:00 +01:00
|
|
|
<?php
|
|
|
|
|
2013-09-05 22:11:02 +02:00
|
|
|
/**
|
|
|
|
* @group file
|
|
|
|
*/
|
2012-10-31 17:57:46 +01:00
|
|
|
final class PhabricatorFile extends PhabricatorFileDAO
|
2013-09-05 22:11:02 +02:00
|
|
|
implements
|
|
|
|
PhabricatorTokenReceiverInterface,
|
|
|
|
PhabricatorSubscribableInterface,
|
|
|
|
PhabricatorPolicyInterface {
|
2011-01-23 03:33:00 +01:00
|
|
|
|
|
|
|
const STORAGE_FORMAT_RAW = 'raw';
|
|
|
|
|
2013-01-07 18:43:35 +01:00
|
|
|
const METADATA_IMAGE_WIDTH = 'width';
|
|
|
|
const METADATA_IMAGE_HEIGHT = 'height';
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
protected $phid;
|
|
|
|
protected $name;
|
|
|
|
protected $mimeType;
|
|
|
|
protected $byteSize;
|
2011-07-08 06:17:00 +02:00
|
|
|
protected $authorPHID;
|
Use a proper entropy source to generate file keys
Summary:
See T549. Under configurations where files are served from an alternate domain
which does not have cookie credentials, we use random keys to prevent browsing,
similar to how Facebook relies on pseudorandom information in image URIs (we
could some day go farther than this and generate file sessions on the alternate
domain or something, I guess).
Currently, we generate these random keys in a roundabout manner. Instead, use a
real entropy source and store the key on the object. This reduces the number of
sha1() calls in the codebase as per T547.
Test Plan: Ran upgrade scripts, verified database was populated correctly.
Configured alternate file domain, uploaded file, verified secret generated and
worked properly. Changed secret, was given 404.
Reviewers: jungejason, benmathews, nh, tuomaspelkonen, aran
Reviewed By: aran
CC: aran, epriestley
Differential Revision: 1036
2011-10-23 22:50:10 +02:00
|
|
|
protected $secretKey;
|
2012-03-20 03:52:24 +01:00
|
|
|
protected $contentHash;
|
2013-01-07 18:43:35 +01:00
|
|
|
protected $metadata = array();
|
2013-09-05 22:11:02 +02:00
|
|
|
protected $mailKey;
|
2011-01-23 03:33:00 +01:00
|
|
|
|
|
|
|
protected $storageEngine;
|
|
|
|
protected $storageFormat;
|
|
|
|
protected $storageHandle;
|
|
|
|
|
2013-02-20 22:33:47 +01:00
|
|
|
protected $ttl;
|
2013-03-19 23:00:44 +01:00
|
|
|
protected $isExplicitUpload = 1;
|
2013-02-20 22:33:47 +01:00
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
public function getConfiguration() {
|
|
|
|
return array(
|
|
|
|
self::CONFIG_AUX_PHID => true,
|
2013-01-07 18:43:35 +01:00
|
|
|
self::CONFIG_SERIALIZATION => array(
|
|
|
|
'metadata' => self::SERIALIZATION_JSON,
|
|
|
|
),
|
2011-01-23 03:33:00 +01:00
|
|
|
) + parent::getConfiguration();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function generatePHID() {
|
2011-03-03 03:58:21 +01:00
|
|
|
return PhabricatorPHID::generateNewPHID(
|
2013-07-22 17:02:56 +02:00
|
|
|
PhabricatorFilePHIDTypeFile::TYPECONST);
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
2013-09-05 22:11:02 +02:00
|
|
|
public function save() {
|
|
|
|
if (!$this->getSecretKey()) {
|
|
|
|
$this->setSecretKey($this->generateSecretKey());
|
|
|
|
}
|
|
|
|
if (!$this->getMailKey()) {
|
|
|
|
$this->setMailKey(Filesystem::readRandomCharacters(20));
|
|
|
|
}
|
|
|
|
return parent::save();
|
|
|
|
}
|
|
|
|
|
2011-10-19 04:46:52 +02:00
|
|
|
public static function readUploadedFileData($spec) {
|
2011-01-23 03:33:00 +01:00
|
|
|
if (!$spec) {
|
|
|
|
throw new Exception("No file was uploaded!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$err = idx($spec, 'error');
|
|
|
|
if ($err) {
|
2011-08-16 21:37:50 +02:00
|
|
|
throw new PhabricatorFileUploadException($err);
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$tmp_name = idx($spec, 'tmp_name');
|
|
|
|
$is_valid = @is_uploaded_file($tmp_name);
|
|
|
|
if (!$is_valid) {
|
|
|
|
throw new Exception("File is not an uploaded file.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$file_data = Filesystem::readFile($tmp_name);
|
|
|
|
$file_size = idx($spec, 'size');
|
|
|
|
|
|
|
|
if (strlen($file_data) != $file_size) {
|
|
|
|
throw new Exception("File size disagrees with uploaded size.");
|
|
|
|
}
|
|
|
|
|
2012-05-07 15:17:00 +02:00
|
|
|
self::validateFileSize(strlen($file_data));
|
|
|
|
|
2011-10-19 04:46:52 +02:00
|
|
|
return $file_data;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function newFromPHPUpload($spec, array $params = array()) {
|
|
|
|
$file_data = self::readUploadedFileData($spec);
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
$file_name = nonempty(
|
|
|
|
idx($params, 'name'),
|
|
|
|
idx($spec, 'name'));
|
|
|
|
$params = array(
|
|
|
|
'name' => $file_name,
|
|
|
|
) + $params;
|
|
|
|
|
|
|
|
return self::newFromFileData($file_data, $params);
|
|
|
|
}
|
|
|
|
|
2012-05-07 15:17:00 +02:00
|
|
|
public static function newFromXHRUpload($data, array $params = array()) {
|
|
|
|
self::validateFileSize(strlen($data));
|
|
|
|
return self::newFromFileData($data, $params);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static function validateFileSize($size) {
|
|
|
|
$limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit');
|
|
|
|
if (!$limit) {
|
|
|
|
return;
|
|
|
|
}
|
2011-01-23 03:33:00 +01:00
|
|
|
|
2012-05-07 15:17:00 +02:00
|
|
|
$limit = phabricator_parse_bytes($limit);
|
|
|
|
if ($size > $limit) {
|
|
|
|
throw new PhabricatorFileUploadException(-1000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-07-09 19:38:25 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a block of data, try to load an existing file with the same content
|
|
|
|
* if one exists. If it does not, build a new file.
|
|
|
|
*
|
|
|
|
* This method is generally used when we have some piece of semi-trusted data
|
|
|
|
* like a diff or a file from a repository that we want to show to the user.
|
|
|
|
* We can't just dump it out because it may be dangerous for any number of
|
|
|
|
* reasons; instead, we need to serve it through the File abstraction so it
|
|
|
|
* ends up on the CDN domain if one is configured and so on. However, if we
|
|
|
|
* simply wrote a new file every time we'd potentially end up with a lot
|
|
|
|
* of redundant data in file storage.
|
|
|
|
*
|
|
|
|
* To solve these problems, we use file storage as a cache and reuse the
|
|
|
|
* same file again if we've previously written it.
|
|
|
|
*
|
|
|
|
* NOTE: This method unguards writes.
|
|
|
|
*
|
|
|
|
* @param string Raw file data.
|
|
|
|
* @param dict Dictionary of file information.
|
|
|
|
*/
|
|
|
|
public static function buildFromFileDataOrHash(
|
|
|
|
$data,
|
|
|
|
array $params = array()) {
|
|
|
|
|
|
|
|
$file = id(new PhabricatorFile())->loadOneWhere(
|
2012-09-21 02:02:59 +02:00
|
|
|
'name = %s AND contentHash = %s LIMIT 1',
|
|
|
|
self::normalizeFileName(idx($params, 'name')),
|
2013-02-15 16:47:50 +01:00
|
|
|
self::hashFileContent($data));
|
2012-07-09 19:38:25 +02:00
|
|
|
|
|
|
|
if (!$file) {
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$file = PhabricatorFile::newFromFileData($data, $params);
|
|
|
|
unset($unguarded);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
public static function newFileFromContentHash($hash, $params) {
|
2012-07-09 19:38:25 +02:00
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
// Check to see if a file with same contentHash exist
|
|
|
|
$file = id(new PhabricatorFile())->loadOneWhere(
|
|
|
|
'contentHash = %s LIMIT 1', $hash);
|
|
|
|
|
|
|
|
if ($file) {
|
|
|
|
// copy storageEngine, storageHandle, storageFormat
|
|
|
|
$copy_of_storage_engine = $file->getStorageEngine();
|
|
|
|
$copy_of_storage_handle = $file->getStorageHandle();
|
|
|
|
$copy_of_storage_format = $file->getStorageFormat();
|
|
|
|
$copy_of_byteSize = $file->getByteSize();
|
|
|
|
$copy_of_mimeType = $file->getMimeType();
|
|
|
|
|
|
|
|
$file_name = idx($params, 'name');
|
|
|
|
$file_name = self::normalizeFileName($file_name);
|
2013-02-20 22:33:47 +01:00
|
|
|
$file_ttl = idx($params, 'ttl');
|
2013-02-09 16:00:52 +01:00
|
|
|
$authorPHID = idx($params, 'authorPHID');
|
|
|
|
|
|
|
|
$new_file = new PhabricatorFile();
|
|
|
|
|
|
|
|
$new_file->setName($file_name);
|
|
|
|
$new_file->setByteSize($copy_of_byteSize);
|
|
|
|
$new_file->setAuthorPHID($authorPHID);
|
2013-02-20 22:33:47 +01:00
|
|
|
$new_file->setTtl($file_ttl);
|
2013-02-09 16:00:52 +01:00
|
|
|
|
|
|
|
$new_file->setContentHash($hash);
|
|
|
|
$new_file->setStorageEngine($copy_of_storage_engine);
|
|
|
|
$new_file->setStorageHandle($copy_of_storage_handle);
|
|
|
|
$new_file->setStorageFormat($copy_of_storage_format);
|
|
|
|
$new_file->setMimeType($copy_of_mimeType);
|
2013-03-19 00:31:38 +01:00
|
|
|
$new_file->copyDimensions($file);
|
2013-02-09 16:00:52 +01:00
|
|
|
|
|
|
|
$new_file->save();
|
|
|
|
|
|
|
|
return $new_file;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static function buildFromFileData($data, array $params = array()) {
|
|
|
|
$selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector');
|
2011-07-20 07:48:38 +02:00
|
|
|
|
2013-02-06 22:37:42 +01:00
|
|
|
if (isset($params['storageEngines'])) {
|
|
|
|
$engines = $params['storageEngines'];
|
|
|
|
} else {
|
|
|
|
$selector = PhabricatorEnv::newObjectFromConfig(
|
|
|
|
'storage.engine-selector');
|
|
|
|
$engines = $selector->selectStorageEngines($data, $params);
|
|
|
|
}
|
|
|
|
|
|
|
|
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
|
2011-07-20 07:48:38 +02:00
|
|
|
if (!$engines) {
|
|
|
|
throw new Exception("No valid storage engines are available!");
|
|
|
|
}
|
|
|
|
|
2012-10-25 20:36:38 +02:00
|
|
|
$file = new PhabricatorFile();
|
|
|
|
|
2011-07-20 07:48:38 +02:00
|
|
|
$data_handle = null;
|
|
|
|
$engine_identifier = null;
|
2012-04-09 00:07:34 +02:00
|
|
|
$exceptions = array();
|
2011-07-20 07:48:38 +02:00
|
|
|
foreach ($engines as $engine) {
|
2012-04-09 00:07:34 +02:00
|
|
|
$engine_class = get_class($engine);
|
2011-07-20 07:48:38 +02:00
|
|
|
try {
|
2012-10-25 20:36:38 +02:00
|
|
|
list($engine_identifier, $data_handle) = $file->writeToEngine(
|
|
|
|
$engine,
|
|
|
|
$data,
|
|
|
|
$params);
|
2011-07-20 07:48:38 +02:00
|
|
|
|
|
|
|
// We stored the file somewhere so stop trying to write it to other
|
|
|
|
// places.
|
|
|
|
break;
|
2012-10-20 16:08:26 +02:00
|
|
|
} catch (PhabricatorFileStorageConfigurationException $ex) {
|
|
|
|
// If an engine is outright misconfigured (or misimplemented), raise
|
|
|
|
// that immediately since it probably needs attention.
|
|
|
|
throw $ex;
|
|
|
|
} catch (Exception $ex) {
|
2011-07-20 07:48:38 +02:00
|
|
|
phlog($ex);
|
2012-04-09 00:07:34 +02:00
|
|
|
|
2012-10-25 20:36:38 +02:00
|
|
|
// If an engine doesn't work, keep trying all the other valid engines
|
|
|
|
// in case something else works.
|
2012-10-20 16:08:26 +02:00
|
|
|
$exceptions[$engine_class] = $ex;
|
2011-07-20 07:48:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$data_handle) {
|
2012-04-09 00:07:34 +02:00
|
|
|
throw new PhutilAggregateException(
|
|
|
|
"All storage engines failed to write file:",
|
|
|
|
$exceptions);
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$file_name = idx($params, 'name');
|
|
|
|
$file_name = self::normalizeFileName($file_name);
|
2013-02-20 22:33:47 +01:00
|
|
|
$file_ttl = idx($params, 'ttl');
|
2011-01-23 03:33:00 +01:00
|
|
|
|
2011-07-08 06:17:00 +02:00
|
|
|
// If for whatever reason, authorPHID isn't passed as a param
|
|
|
|
// (always the case with newFromFileDownload()), store a ''
|
|
|
|
$authorPHID = idx($params, 'authorPHID');
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
$file->setName($file_name);
|
|
|
|
$file->setByteSize(strlen($data));
|
2011-07-08 06:17:00 +02:00
|
|
|
$file->setAuthorPHID($authorPHID);
|
2013-02-20 22:33:47 +01:00
|
|
|
$file->setTtl($file_ttl);
|
2013-02-15 16:47:50 +01:00
|
|
|
$file->setContentHash(self::hashFileContent($data));
|
2011-01-23 03:33:00 +01:00
|
|
|
|
2011-07-20 07:48:38 +02:00
|
|
|
$file->setStorageEngine($engine_identifier);
|
|
|
|
$file->setStorageHandle($data_handle);
|
2011-01-23 03:33:00 +01:00
|
|
|
|
2011-07-20 07:48:38 +02:00
|
|
|
// TODO: This is probably YAGNI, but allows for us to do encryption or
|
|
|
|
// compression later if we want.
|
2011-01-23 03:33:00 +01:00
|
|
|
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
|
2013-03-22 12:59:50 +01:00
|
|
|
$file->setIsExplicitUpload(idx($params, 'isExplicitUpload') ? 1 : 0);
|
2011-01-23 03:33:00 +01:00
|
|
|
|
2011-02-02 22:48:52 +01:00
|
|
|
if (isset($params['mime-type'])) {
|
|
|
|
$file->setMimeType($params['mime-type']);
|
|
|
|
} else {
|
2012-02-15 02:00:05 +01:00
|
|
|
$tmp = new TempFile();
|
|
|
|
Filesystem::writeFile($tmp, $data);
|
|
|
|
$file->setMimeType(Filesystem::getMimeType($tmp));
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
2013-01-07 18:43:35 +01:00
|
|
|
try {
|
|
|
|
$file->updateDimensions(false);
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
// Do nothing
|
|
|
|
}
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
$file->save();
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
public static function newFromFileData($data, array $params = array()) {
|
|
|
|
$hash = self::hashFileContent($data);
|
|
|
|
$file = self::newFileFromContentHash($hash, $params);
|
|
|
|
|
|
|
|
if ($file) {
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::buildFromFileData($data, $params);
|
|
|
|
}
|
|
|
|
|
2012-10-25 20:36:38 +02:00
|
|
|
public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
|
|
|
|
if (!$this->getID() || !$this->getStorageHandle()) {
|
|
|
|
throw new Exception(
|
|
|
|
"You can not migrate a file which hasn't yet been saved.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = $this->loadFileData();
|
|
|
|
$params = array(
|
|
|
|
'name' => $this->getName(),
|
|
|
|
);
|
|
|
|
|
|
|
|
list($new_identifier, $new_handle) = $this->writeToEngine(
|
|
|
|
$engine,
|
|
|
|
$data,
|
|
|
|
$params);
|
|
|
|
|
|
|
|
$old_engine = $this->instantiateStorageEngine();
|
|
|
|
$old_handle = $this->getStorageHandle();
|
|
|
|
|
|
|
|
$this->setStorageEngine($new_identifier);
|
|
|
|
$this->setStorageHandle($new_handle);
|
|
|
|
$this->save();
|
|
|
|
|
|
|
|
$old_engine->deleteFile($old_handle);
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function writeToEngine(
|
|
|
|
PhabricatorFileStorageEngine $engine,
|
|
|
|
$data,
|
|
|
|
array $params) {
|
|
|
|
|
|
|
|
$engine_class = get_class($engine);
|
|
|
|
|
|
|
|
$data_handle = $engine->writeFile($data, $params);
|
|
|
|
|
|
|
|
if (!$data_handle || strlen($data_handle) > 255) {
|
|
|
|
// This indicates an improperly implemented storage engine.
|
|
|
|
throw new PhabricatorFileStorageConfigurationException(
|
|
|
|
"Storage engine '{$engine_class}' executed writeFile() but did ".
|
|
|
|
"not return a valid handle ('{$data_handle}') to the data: it ".
|
|
|
|
"must be nonempty and no longer than 255 characters.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$engine_identifier = $engine->getEngineIdentifier();
|
|
|
|
if (!$engine_identifier || strlen($engine_identifier) > 32) {
|
|
|
|
throw new PhabricatorFileStorageConfigurationException(
|
|
|
|
"Storage engine '{$engine_class}' returned an improper engine ".
|
|
|
|
"identifier '{$engine_identifier}': it must be nonempty ".
|
|
|
|
"and no longer than 32 characters.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return array($engine_identifier, $data_handle);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-02-21 21:43:39 +01:00
|
|
|
public static function newFromFileDownload($uri, array $params = array()) {
|
|
|
|
// Make sure we're allowed to make a request first
|
|
|
|
if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) {
|
|
|
|
throw new Exception("Outbound HTTP requests are disabled!");
|
|
|
|
}
|
|
|
|
|
2011-04-14 00:15:48 +02:00
|
|
|
$uri = new PhutilURI($uri);
|
2011-05-02 23:20:24 +02:00
|
|
|
|
|
|
|
$protocol = $uri->getProtocol();
|
|
|
|
switch ($protocol) {
|
|
|
|
case 'http':
|
|
|
|
case 'https':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// Make sure we are not accessing any file:// URIs or similar.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2012-06-19 00:11:47 +02:00
|
|
|
$timeout = 5;
|
|
|
|
|
2013-02-05 00:28:09 +01:00
|
|
|
list($file_data) = id(new HTTPSFuture($uri))
|
|
|
|
->setTimeout($timeout)
|
|
|
|
->resolvex();
|
2011-04-14 00:15:48 +02:00
|
|
|
|
2013-02-21 21:43:39 +01:00
|
|
|
$params = $params + array(
|
|
|
|
'name' => basename($uri),
|
|
|
|
);
|
|
|
|
|
2013-02-05 00:28:09 +01:00
|
|
|
return self::newFromFileData($file_data, $params);
|
2011-04-14 00:15:48 +02:00
|
|
|
}
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
public static function normalizeFileName($file_name) {
|
2013-03-22 18:51:20 +01:00
|
|
|
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
|
|
|
|
$file_name = preg_replace($pattern, '_', $file_name);
|
|
|
|
$file_name = preg_replace('@_+@', '_', $file_name);
|
|
|
|
$file_name = trim($file_name, '_');
|
|
|
|
|
|
|
|
$disallowed_filenames = array(
|
|
|
|
'.' => 'dot',
|
|
|
|
'..' => 'dotdot',
|
|
|
|
'' => 'file',
|
|
|
|
);
|
|
|
|
$file_name = idx($disallowed_filenames, $file_name, $file_name);
|
|
|
|
|
|
|
|
return $file_name;
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function delete() {
|
2013-02-20 22:33:47 +01:00
|
|
|
|
When deleting a file, delete all transformations of the file
Summary:
Fixes T3143. When a user deletes a file, delete all transforms of the file too. In particular, this means that deleting an image deletes all the thumbnails of the image.
In most cases, this aligns with user expectations. The only sort of weird case I can come up with is that memes are transformations of the source macro image, so deleting grumpycat will delete all the hilarious grumpycat memes. This seems not-too-unreasonable, though, and desirable if someone accidentally uploads an inappropriate image which is promptly turned into a meme.
Test Plan:
Added a unit test which covers both inbound and outbound transformations.
Uploaded a file and deleted it, verified its thumbnail was also deleted.
Reviewers: chad, btrahan, joseph.kampf
Reviewed By: btrahan
CC: aran, joseph.kampf
Maniphest Tasks: T3143
Differential Revision: https://secure.phabricator.com/D5879
2013-05-10 01:08:35 +02:00
|
|
|
// We want to delete all the rows which mark this file as the transformation
|
|
|
|
// of some other file (since we're getting rid of it). We also delete all
|
|
|
|
// the transformations of this file, so that a user who deletes an image
|
|
|
|
// doesn't need to separately hunt down and delete a bunch of thumbnails and
|
|
|
|
// resizes of it.
|
|
|
|
|
|
|
|
$outbound_xforms = id(new PhabricatorFileQuery())
|
|
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
|
|
|
->withTransforms(
|
|
|
|
array(
|
|
|
|
array(
|
|
|
|
'originalPHID' => $this->getPHID(),
|
|
|
|
'transform' => true,
|
|
|
|
),
|
|
|
|
))
|
|
|
|
->execute();
|
|
|
|
|
|
|
|
foreach ($outbound_xforms as $outbound_xform) {
|
|
|
|
$outbound_xform->delete();
|
2013-02-20 22:33:47 +01:00
|
|
|
}
|
When deleting a file, delete all transformations of the file
Summary:
Fixes T3143. When a user deletes a file, delete all transforms of the file too. In particular, this means that deleting an image deletes all the thumbnails of the image.
In most cases, this aligns with user expectations. The only sort of weird case I can come up with is that memes are transformations of the source macro image, so deleting grumpycat will delete all the hilarious grumpycat memes. This seems not-too-unreasonable, though, and desirable if someone accidentally uploads an inappropriate image which is promptly turned into a meme.
Test Plan:
Added a unit test which covers both inbound and outbound transformations.
Uploaded a file and deleted it, verified its thumbnail was also deleted.
Reviewers: chad, btrahan, joseph.kampf
Reviewed By: btrahan
CC: aran, joseph.kampf
Maniphest Tasks: T3143
Differential Revision: https://secure.phabricator.com/D5879
2013-05-10 01:08:35 +02:00
|
|
|
|
|
|
|
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
|
|
|
|
'transformedPHID = %s',
|
|
|
|
$this->getPHID());
|
|
|
|
|
|
|
|
$this->openTransaction();
|
|
|
|
foreach ($inbound_xforms as $inbound_xform) {
|
|
|
|
$inbound_xform->delete();
|
|
|
|
}
|
|
|
|
$ret = parent::delete();
|
2013-02-20 22:33:47 +01:00
|
|
|
$this->saveTransaction();
|
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
// Check to see if other files are using storage
|
|
|
|
$other_file = id(new PhabricatorFile())->loadAllWhere(
|
|
|
|
'storageEngine = %s AND storageHandle = %s AND
|
When deleting a file, delete all transformations of the file
Summary:
Fixes T3143. When a user deletes a file, delete all transforms of the file too. In particular, this means that deleting an image deletes all the thumbnails of the image.
In most cases, this aligns with user expectations. The only sort of weird case I can come up with is that memes are transformations of the source macro image, so deleting grumpycat will delete all the hilarious grumpycat memes. This seems not-too-unreasonable, though, and desirable if someone accidentally uploads an inappropriate image which is promptly turned into a meme.
Test Plan:
Added a unit test which covers both inbound and outbound transformations.
Uploaded a file and deleted it, verified its thumbnail was also deleted.
Reviewers: chad, btrahan, joseph.kampf
Reviewed By: btrahan
CC: aran, joseph.kampf
Maniphest Tasks: T3143
Differential Revision: https://secure.phabricator.com/D5879
2013-05-10 01:08:35 +02:00
|
|
|
storageFormat = %s AND id != %d LIMIT 1',
|
|
|
|
$this->getStorageEngine(),
|
|
|
|
$this->getStorageHandle(),
|
|
|
|
$this->getStorageFormat(),
|
2013-02-09 16:00:52 +01:00
|
|
|
$this->getID());
|
|
|
|
|
|
|
|
// If this is the only file using the storage, delete storage
|
When deleting a file, delete all transformations of the file
Summary:
Fixes T3143. When a user deletes a file, delete all transforms of the file too. In particular, this means that deleting an image deletes all the thumbnails of the image.
In most cases, this aligns with user expectations. The only sort of weird case I can come up with is that memes are transformations of the source macro image, so deleting grumpycat will delete all the hilarious grumpycat memes. This seems not-too-unreasonable, though, and desirable if someone accidentally uploads an inappropriate image which is promptly turned into a meme.
Test Plan:
Added a unit test which covers both inbound and outbound transformations.
Uploaded a file and deleted it, verified its thumbnail was also deleted.
Reviewers: chad, btrahan, joseph.kampf
Reviewed By: btrahan
CC: aran, joseph.kampf
Maniphest Tasks: T3143
Differential Revision: https://secure.phabricator.com/D5879
2013-05-10 01:08:35 +02:00
|
|
|
if (!$other_file) {
|
2013-02-09 16:00:52 +01:00
|
|
|
$engine = $this->instantiateStorageEngine();
|
2013-05-29 15:28:57 +02:00
|
|
|
try {
|
|
|
|
$engine->deleteFile($this->getStorageHandle());
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
// In the worst case, we're leaving some data stranded in a storage
|
|
|
|
// engine, which is fine.
|
|
|
|
phlog($ex);
|
|
|
|
}
|
2013-02-09 16:00:52 +01:00
|
|
|
}
|
When deleting a file, delete all transformations of the file
Summary:
Fixes T3143. When a user deletes a file, delete all transforms of the file too. In particular, this means that deleting an image deletes all the thumbnails of the image.
In most cases, this aligns with user expectations. The only sort of weird case I can come up with is that memes are transformations of the source macro image, so deleting grumpycat will delete all the hilarious grumpycat memes. This seems not-too-unreasonable, though, and desirable if someone accidentally uploads an inappropriate image which is promptly turned into a meme.
Test Plan:
Added a unit test which covers both inbound and outbound transformations.
Uploaded a file and deleted it, verified its thumbnail was also deleted.
Reviewers: chad, btrahan, joseph.kampf
Reviewed By: btrahan
CC: aran, joseph.kampf
Maniphest Tasks: T3143
Differential Revision: https://secure.phabricator.com/D5879
2013-05-10 01:08:35 +02:00
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
public static function hashFileContent($data) {
|
2013-02-15 16:47:50 +01:00
|
|
|
return sha1($data);
|
2013-02-09 16:00:52 +01:00
|
|
|
}
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
public function loadFileData() {
|
|
|
|
|
2011-07-20 07:48:38 +02:00
|
|
|
$engine = $this->instantiateStorageEngine();
|
|
|
|
$data = $engine->readFile($this->getStorageHandle());
|
2011-01-23 03:33:00 +01:00
|
|
|
|
|
|
|
switch ($this->getStorageFormat()) {
|
|
|
|
case self::STORAGE_FORMAT_RAW:
|
|
|
|
$data = $data;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception("Unknown storage format.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2011-01-26 18:02:09 +01:00
|
|
|
public function getViewURI() {
|
Use a proper entropy source to generate file keys
Summary:
See T549. Under configurations where files are served from an alternate domain
which does not have cookie credentials, we use random keys to prevent browsing,
similar to how Facebook relies on pseudorandom information in image URIs (we
could some day go farther than this and generate file sessions on the alternate
domain or something, I guess).
Currently, we generate these random keys in a roundabout manner. Instead, use a
real entropy source and store the key on the object. This reduces the number of
sha1() calls in the codebase as per T547.
Test Plan: Ran upgrade scripts, verified database was populated correctly.
Configured alternate file domain, uploaded file, verified secret generated and
worked properly. Changed secret, was given 404.
Reviewers: jungejason, benmathews, nh, tuomaspelkonen, aran
Reviewed By: aran
CC: aran, epriestley
Differential Revision: 1036
2011-10-23 22:50:10 +02:00
|
|
|
if (!$this->getPHID()) {
|
|
|
|
throw new Exception(
|
|
|
|
"You must save a file before you can generate a view URI.");
|
|
|
|
}
|
|
|
|
|
2012-01-17 01:20:18 +01:00
|
|
|
$name = phutil_escape_uri($this->getName());
|
|
|
|
|
Move ALL files to serve from the alternate file domain, not just files without
"Content-Disposition: attachment"
Summary:
We currently serve some files off the primary domain (with "Content-Disposition:
attachment" + a CSRF check) and some files off the alternate domain (without
either).
This is not sufficient, because some UAs (like the iPad) ignore
"Content-Disposition: attachment". So there's an attack that goes like this:
- Alice uploads xss.html
- Alice says to Bob "hey download this file on your iPad"
- Bob clicks "Download" on Phabricator on his iPad, gets XSS'd.
NOTE: This removes the CSRF check for downloading files. The check is nice to
have but only raises the barrier to entry slightly. Between iPad / sniffing /
flash bytecode attacks, single-domain installs are simply insecure. We could
restore the check at some point in conjunction with a derived authentication
cookie (i.e., a mini-session-token which is only useful for downloading files),
but that's a lot of complexity to drop all at once.
(Because files are now authenticated only by knowing the PHID and secret key,
this also fixes the "no profile pictures in public feed while logged out"
issue.)
Test Plan: Viewed, info'd, and downloaded files
Reviewers: btrahan, arice, alok
Reviewed By: arice
CC: aran, epriestley
Maniphest Tasks: T843
Differential Revision: https://secure.phabricator.com/D1608
2012-02-14 23:52:27 +01:00
|
|
|
$path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
2011-01-26 18:02:09 +01:00
|
|
|
}
|
2011-02-22 18:22:57 +01:00
|
|
|
|
2011-07-29 19:00:16 +02:00
|
|
|
public function getInfoURI() {
|
|
|
|
return '/file/info/'.$this->getPHID().'/';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getBestURI() {
|
|
|
|
if ($this->isViewableInBrowser()) {
|
|
|
|
return $this->getViewURI();
|
|
|
|
} else {
|
|
|
|
return $this->getInfoURI();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-10-23 04:06:56 +02:00
|
|
|
public function getDownloadURI() {
|
|
|
|
$uri = id(new PhutilURI($this->getViewURI()))
|
|
|
|
->setQueryParam('download', true);
|
|
|
|
return (string) $uri;
|
|
|
|
}
|
|
|
|
|
2013-06-17 16:08:50 +02:00
|
|
|
public function getProfileThumbURI() {
|
|
|
|
$path = '/file/xform/thumb-profile/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
|
|
|
}
|
|
|
|
|
Improve drag-and-drop uploader
Summary:
Make it discoverable, show uploading progress, show file thumbnails, allow you
to remove files, make it a generic form component.
Test Plan:
Uploaded ducks
Reviewed By: tomo
Reviewers: aran, tomo, jungejason, tuomaspelkonen
CC: anjali, aran, epriestley, tomo
Differential Revision: 334
2011-05-23 01:11:41 +02:00
|
|
|
public function getThumb60x45URI() {
|
2012-10-23 04:06:56 +02:00
|
|
|
$path = '/file/xform/thumb-60x45/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
Improve drag-and-drop uploader
Summary:
Make it discoverable, show uploading progress, show file thumbnails, allow you
to remove files, make it a generic form component.
Test Plan:
Uploaded ducks
Reviewed By: tomo
Reviewers: aran, tomo, jungejason, tuomaspelkonen
CC: anjali, aran, epriestley, tomo
Differential Revision: 334
2011-05-23 01:11:41 +02:00
|
|
|
}
|
|
|
|
|
2011-05-23 02:06:42 +02:00
|
|
|
public function getThumb160x120URI() {
|
2012-10-23 04:06:56 +02:00
|
|
|
$path = '/file/xform/thumb-160x120/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
2011-05-23 02:06:42 +02:00
|
|
|
}
|
|
|
|
|
2013-02-22 15:53:54 +01:00
|
|
|
public function getPreview140URI() {
|
|
|
|
$path = '/file/xform/preview-140/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
|
|
|
}
|
|
|
|
|
2012-10-08 22:26:10 +02:00
|
|
|
public function getPreview220URI() {
|
2012-10-23 04:06:56 +02:00
|
|
|
$path = '/file/xform/preview-220/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
2012-10-08 22:26:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getThumb220x165URI() {
|
2012-10-23 04:06:56 +02:00
|
|
|
$path = '/file/xform/thumb-220x165/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
2012-10-08 22:26:10 +02:00
|
|
|
}
|
2011-05-23 02:06:42 +02:00
|
|
|
|
2013-03-05 17:05:51 +01:00
|
|
|
public function getThumb280x210URI() {
|
|
|
|
$path = '/file/xform/thumb-280x210/'.$this->getPHID().'/'
|
|
|
|
.$this->getSecretKey().'/';
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
|
|
|
}
|
|
|
|
|
2011-02-22 18:19:14 +01:00
|
|
|
public function isViewableInBrowser() {
|
|
|
|
return ($this->getViewableMimeType() !== null);
|
|
|
|
}
|
2011-02-22 18:22:57 +01:00
|
|
|
|
2012-03-20 03:52:24 +01:00
|
|
|
public function isViewableImage() {
|
|
|
|
if (!$this->isViewableInBrowser()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
|
|
|
|
$mime_type = $this->getMimeType();
|
|
|
|
return idx($mime_map, $mime_type);
|
|
|
|
}
|
|
|
|
|
2011-05-22 23:40:51 +02:00
|
|
|
public function isTransformableImage() {
|
Support thumbnailing non-image files and straighten out setup for 'gd'
Summary:
Make 'gd' an explicit optional dependency, test for it in setup, and make the
software behave correctly if it is not available.
When generating file thumnails, provide reasonable defaults and behavior for
non-image files.
Test Plan:
Uploaded text files, pdf files, etc., and got real thumbnails instead of a
broken image.
Simulated setup and gd failures and walked through setup process and image
fallback for thumbnails.
Reviewed By: aran
Reviewers: toulouse, jungejason, tuomaspelkonen, aran
CC: aran, epriestley
Differential Revision: 446
2011-06-13 17:43:42 +02:00
|
|
|
|
|
|
|
// NOTE: The way the 'gd' extension works in PHP is that you can install it
|
|
|
|
// with support for only some file types, so it might be able to handle
|
|
|
|
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
|
|
|
|
// warns you if you don't have complete support.
|
|
|
|
|
|
|
|
$matches = null;
|
|
|
|
$ok = preg_match(
|
|
|
|
'@^image/(gif|png|jpe?g)@',
|
|
|
|
$this->getViewableMimeType(),
|
|
|
|
$matches);
|
|
|
|
if (!$ok) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch ($matches[1]) {
|
|
|
|
case 'jpg';
|
|
|
|
case 'jpeg':
|
|
|
|
return function_exists('imagejpeg');
|
|
|
|
break;
|
|
|
|
case 'png':
|
|
|
|
return function_exists('imagepng');
|
|
|
|
break;
|
|
|
|
case 'gif':
|
|
|
|
return function_exists('imagegif');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception('Unknown type matched as image MIME type.');
|
|
|
|
}
|
2011-05-22 23:40:51 +02:00
|
|
|
}
|
|
|
|
|
2012-03-14 20:41:33 +01:00
|
|
|
public static function getTransformableImageFormats() {
|
|
|
|
$supported = array();
|
|
|
|
|
|
|
|
if (function_exists('imagejpeg')) {
|
|
|
|
$supported[] = 'jpg';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (function_exists('imagepng')) {
|
|
|
|
$supported[] = 'png';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (function_exists('imagegif')) {
|
|
|
|
$supported[] = 'gif';
|
|
|
|
}
|
|
|
|
|
|
|
|
return $supported;
|
|
|
|
}
|
|
|
|
|
2011-07-20 07:48:38 +02:00
|
|
|
protected function instantiateStorageEngine() {
|
2012-10-25 20:36:38 +02:00
|
|
|
return self::buildEngine($this->getStorageEngine());
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function buildEngine($engine_identifier) {
|
|
|
|
$engines = self::buildAllEngines();
|
|
|
|
foreach ($engines as $engine) {
|
|
|
|
if ($engine->getEngineIdentifier() == $engine_identifier) {
|
|
|
|
return $engine;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Exception(
|
|
|
|
"Storage engine '{$engine_identifier}' could not be located!");
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function buildAllEngines() {
|
2011-07-20 07:48:38 +02:00
|
|
|
$engines = id(new PhutilSymbolLoader())
|
|
|
|
->setType('class')
|
2012-10-25 20:36:38 +02:00
|
|
|
->setConcreteOnly(true)
|
2011-07-20 07:48:38 +02:00
|
|
|
->setAncestorClass('PhabricatorFileStorageEngine')
|
|
|
|
->selectAndLoadSymbols();
|
|
|
|
|
2012-10-25 20:36:38 +02:00
|
|
|
$results = array();
|
2011-07-20 07:48:38 +02:00
|
|
|
foreach ($engines as $engine_class) {
|
2012-10-25 20:36:38 +02:00
|
|
|
$results[] = newv($engine_class['name'], array());
|
2011-07-20 07:48:38 +02:00
|
|
|
}
|
|
|
|
|
2012-10-25 20:36:38 +02:00
|
|
|
return $results;
|
2011-07-20 07:48:38 +02:00
|
|
|
}
|
|
|
|
|
2011-02-22 18:19:14 +01:00
|
|
|
public function getViewableMimeType() {
|
|
|
|
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
|
|
|
|
|
|
|
|
$mime_type = $this->getMimeType();
|
|
|
|
$mime_parts = explode(';', $mime_type);
|
2011-02-28 19:15:42 +01:00
|
|
|
$mime_type = trim(reset($mime_parts));
|
2011-02-22 18:22:57 +01:00
|
|
|
|
2011-02-22 18:19:14 +01:00
|
|
|
return idx($mime_map, $mime_type);
|
|
|
|
}
|
2011-01-26 18:02:09 +01:00
|
|
|
|
2013-03-16 07:41:36 +01:00
|
|
|
public function getDisplayIconForMimeType() {
|
|
|
|
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
|
|
|
|
$mime_type = $this->getMimeType();
|
|
|
|
return idx($mime_map, $mime_type, 'docs_file');
|
|
|
|
}
|
|
|
|
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
public function validateSecretKey($key) {
|
Use a proper entropy source to generate file keys
Summary:
See T549. Under configurations where files are served from an alternate domain
which does not have cookie credentials, we use random keys to prevent browsing,
similar to how Facebook relies on pseudorandom information in image URIs (we
could some day go farther than this and generate file sessions on the alternate
domain or something, I guess).
Currently, we generate these random keys in a roundabout manner. Instead, use a
real entropy source and store the key on the object. This reduces the number of
sha1() calls in the codebase as per T547.
Test Plan: Ran upgrade scripts, verified database was populated correctly.
Configured alternate file domain, uploaded file, verified secret generated and
worked properly. Changed secret, was given 404.
Reviewers: jungejason, benmathews, nh, tuomaspelkonen, aran
Reviewed By: aran
CC: aran, epriestley
Differential Revision: 1036
2011-10-23 22:50:10 +02:00
|
|
|
return ($key == $this->getSecretKey());
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
}
|
|
|
|
|
Use a proper entropy source to generate file keys
Summary:
See T549. Under configurations where files are served from an alternate domain
which does not have cookie credentials, we use random keys to prevent browsing,
similar to how Facebook relies on pseudorandom information in image URIs (we
could some day go farther than this and generate file sessions on the alternate
domain or something, I guess).
Currently, we generate these random keys in a roundabout manner. Instead, use a
real entropy source and store the key on the object. This reduces the number of
sha1() calls in the codebase as per T547.
Test Plan: Ran upgrade scripts, verified database was populated correctly.
Configured alternate file domain, uploaded file, verified secret generated and
worked properly. Changed secret, was given 404.
Reviewers: jungejason, benmathews, nh, tuomaspelkonen, aran
Reviewed By: aran
CC: aran, epriestley
Differential Revision: 1036
2011-10-23 22:50:10 +02:00
|
|
|
public function generateSecretKey() {
|
|
|
|
return Filesystem::readRandomCharacters(20);
|
|
|
|
}
|
2012-10-31 17:57:46 +01:00
|
|
|
|
2013-01-07 18:43:35 +01:00
|
|
|
public function updateDimensions($save = true) {
|
|
|
|
if (!$this->isViewableImage()) {
|
|
|
|
throw new Exception(
|
|
|
|
"This file is not a viewable image.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!function_exists("imagecreatefromstring")) {
|
|
|
|
throw new Exception(
|
|
|
|
"Cannot retrieve image information.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = $this->loadFileData();
|
|
|
|
|
|
|
|
$img = imagecreatefromstring($data);
|
|
|
|
if ($img === false) {
|
|
|
|
throw new Exception(
|
|
|
|
"Error when decoding image.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
|
|
|
|
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
|
|
|
|
|
|
|
|
if ($save) {
|
|
|
|
$this->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2013-03-19 00:31:38 +01:00
|
|
|
public function copyDimensions(PhabricatorFile $file) {
|
|
|
|
$metadata = $file->getMetadata();
|
|
|
|
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
|
|
|
|
if ($width) {
|
|
|
|
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
|
|
|
|
}
|
|
|
|
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
|
|
|
|
if ($height) {
|
|
|
|
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2013-01-07 18:43:35 +01:00
|
|
|
public static function getMetadataName($metadata) {
|
|
|
|
switch ($metadata) {
|
|
|
|
case self::METADATA_IMAGE_WIDTH:
|
|
|
|
$name = pht('Width');
|
|
|
|
break;
|
|
|
|
case self::METADATA_IMAGE_HEIGHT:
|
|
|
|
$name = pht('Height');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$name = ucfirst($metadata);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $name;
|
|
|
|
}
|
|
|
|
|
2012-10-31 17:57:46 +01:00
|
|
|
|
Provide "builtin" files and use them to fix Pholio when files are deleted
Summary:
Fixes T3132. Currently, if a user deletes a file which is present in a mock, that mock throws an exception when loading. If the file is also the cover photo, the mock list throws an exception as well.
In other applications, we can sometimes deal with this (a sub-object vanishing) by implicitly hiding the parent object (for example, we can just vanish feed stories about objects which no longer exist). We can also sometimes deal with it by preventing sub-objects from being directly deleted.
However, neither approach is reasonable in this case.
If we vanish the whole mock, we'll lose all the comments and it will generally be weird. Vanishing a mock is a big deal compared to vanishing a feed story. We'll also need to load more data on the list view to prevent showing a mock on the list view and then realizing we need to vanish it on the detail view (because all of its images have been deleted).
We permit total deletion of files to allow users to recover from accidentally uploading sensitive files (which has happened a few times), and I'm hesitant to remove this capability because I think it serves a real need, so we can't prevent sub-objects from being deleted.
So we're left in a relatively unique situation. To solve this, I've added a "builtin" mechanism, which allows us to expose some resource we ship with as a PhabricatorFile. Then we just swap it out in place of the original file and proceed forward normally, as though nothing happened. The user sees a placeholder image instead of the original, but everything else works reasonably and this seems like a fairly acceptable outcome.
I believe we can use this mechanism to simplify some other code too, like default profile pictures.
Test Plan: Deleted a Pholio mock cover image's file. Implemented change, saw functional Pholio again with beautiful life-affirming "?" art replacing soul-shattering exception.
Reviewers: btrahan, chad
Reviewed By: chad
CC: aran
Maniphest Tasks: T3132
Differential Revision: https://secure.phabricator.com/D5870
2013-05-09 03:12:52 +02:00
|
|
|
/**
|
|
|
|
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
|
|
|
|
* resources. The builtin mechanism allows files shipped with Phabricator
|
|
|
|
* to be treated like normal files so that APIs do not need to special case
|
|
|
|
* things like default images or deleted files.
|
|
|
|
*
|
|
|
|
* Builtins are located in `resources/builtin/` and identified by their
|
|
|
|
* name.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser Viewing user.
|
|
|
|
* @param list<string> List of builtin file names.
|
|
|
|
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
|
|
|
|
*/
|
|
|
|
public static function loadBuiltins(PhabricatorUser $user, array $names) {
|
|
|
|
$specs = array();
|
|
|
|
foreach ($names as $name) {
|
|
|
|
$specs[] = array(
|
|
|
|
'originalPHID' => PhabricatorPHIDConstants::PHID_VOID,
|
|
|
|
'transform' => 'builtin:'.$name,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$files = id(new PhabricatorFileQuery())
|
|
|
|
->setViewer($user)
|
|
|
|
->withTransforms($specs)
|
|
|
|
->execute();
|
|
|
|
|
|
|
|
$files = mpull($files, null, 'getName');
|
|
|
|
|
|
|
|
$root = dirname(phutil_get_library_root('phabricator'));
|
|
|
|
$root = $root.'/resources/builtin/';
|
|
|
|
|
|
|
|
$build = array();
|
|
|
|
foreach ($names as $name) {
|
|
|
|
if (isset($files[$name])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is just a sanity check to prevent loading arbitrary files.
|
|
|
|
if (basename($name) != $name) {
|
|
|
|
throw new Exception("Invalid builtin name '{$name}'!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$path = $root.$name;
|
|
|
|
|
|
|
|
if (!Filesystem::pathExists($path)) {
|
|
|
|
throw new Exception("Builtin '{$path}' does not exist!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = Filesystem::readFile($path);
|
|
|
|
$params = array(
|
|
|
|
'name' => $name,
|
|
|
|
'ttl' => time() + (60 * 60 * 24 * 7),
|
|
|
|
);
|
|
|
|
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$file = PhabricatorFile::newFromFileData($data, $params);
|
|
|
|
$xform = id(new PhabricatorTransformedFile())
|
|
|
|
->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID)
|
|
|
|
->setTransform('builtin:'.$name)
|
|
|
|
->setTransformedPHID($file->getPHID())
|
|
|
|
->save();
|
|
|
|
unset($unguarded);
|
|
|
|
|
|
|
|
$files[$name] = $file;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $files;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convenience wrapper for @{method:loadBuiltins}.
|
|
|
|
*
|
|
|
|
* @param PhabricatorUser Viewing user.
|
|
|
|
* @param string Single builtin name to load.
|
|
|
|
* @return PhabricatorFile Corresponding builtin file.
|
|
|
|
*/
|
|
|
|
public static function loadBuiltin(PhabricatorUser $user, $name) {
|
|
|
|
return idx(self::loadBuiltins($user, array($name)), $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-10-31 17:57:46 +01:00
|
|
|
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function getCapabilities() {
|
|
|
|
return array(
|
|
|
|
PhabricatorPolicyCapability::CAN_VIEW,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPolicy($capability) {
|
|
|
|
// TODO: Implement proper per-object policies.
|
|
|
|
return PhabricatorPolicies::POLICY_USER;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-09-27 17:43:41 +02:00
|
|
|
public function describeAutomaticCapability($capability) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2013-09-05 22:11:02 +02:00
|
|
|
|
|
|
|
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function isAutomaticallySubscribed($phid) {
|
|
|
|
return ($this->authorPHID == $phid);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function getUsersToNotifyOfTokenGiven() {
|
|
|
|
return array(
|
|
|
|
$this->getAuthorPHID(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|