2011-01-23 03:33:00 +01:00
|
|
|
<?php
|
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
/**
|
|
|
|
* Parameters
|
|
|
|
* ==========
|
|
|
|
*
|
|
|
|
* When creating a new file using a method like @{method:newFromFileData}, these
|
|
|
|
* parameters are supported:
|
|
|
|
*
|
|
|
|
* | name | Human readable filename.
|
|
|
|
* | authorPHID | User PHID of uploader.
|
|
|
|
* | ttl | Temporary file lifetime, in seconds.
|
|
|
|
* | viewPolicy | File visibility policy.
|
|
|
|
* | isExplicitUpload | Used to show users files they explicitly uploaded.
|
|
|
|
* | canCDN | Allows the file to be cached and delivered over a CDN.
|
|
|
|
* | mime-type | Optional, explicit file MIME type.
|
2015-03-01 21:12:38 +01:00
|
|
|
* | builtin | Optional filename, identifies this as a builtin.
|
2014-08-11 18:39:40 +02:00
|
|
|
*
|
|
|
|
*/
|
2012-10-31 17:57:46 +01:00
|
|
|
final class PhabricatorFile extends PhabricatorFileDAO
|
2013-09-05 22:11:02 +02:00
|
|
|
implements
|
2014-12-03 22:16:15 +01:00
|
|
|
PhabricatorApplicationTransactionInterface,
|
2013-09-05 22:11:02 +02:00
|
|
|
PhabricatorTokenReceiverInterface,
|
|
|
|
PhabricatorSubscribableInterface,
|
2013-10-25 21:52:00 +02:00
|
|
|
PhabricatorFlaggableInterface,
|
2014-08-20 00:44:27 +02:00
|
|
|
PhabricatorPolicyInterface,
|
|
|
|
PhabricatorDestructibleInterface {
|
2011-01-23 03:33:00 +01:00
|
|
|
|
2014-08-08 03:56:19 +02:00
|
|
|
const ONETIME_TEMPORARY_TOKEN_TYPE = 'file:onetime';
|
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';
|
2014-08-14 21:13:26 +02:00
|
|
|
const METADATA_CAN_CDN = 'canCDN';
|
2015-03-01 21:12:38 +01:00
|
|
|
const METADATA_BUILTIN = 'builtin';
|
Add a chunking storage engine for files
Summary:
Ref T7149. This isn't complete and isn't active yet, but does basically work. I'll shore it up in the next few diffs.
The new workflow goes like this:
> Client, file.allocate(): I'd like to upload a file with length L, metadata M, and hash H.
Then the server returns `upload` (a boolean) and `filePHID` (a PHID). These mean:
| upload | filePHID | means |
|---|---|---|
| false | false | Server can't accept file.
| false | true | File data already known, file created from hash.
| true | false | Just upload normally.
| true | true | Query chunks to start or resume a chunked upload.
All but the last case are uninteresting and work like exising uploads with `file.uploadhash` (which we can eventually deprecate).
In the last case:
> Client, file.querychunks(): Give me a list of chunks that I should upload.
This returns all the chunks for the file. Chunks have a start byte, an end byte, and a "complete" flag to indicate that the server already has the data.
Then, the client fills in chunks by sending them:
> Client, file.uploadchunk(): Here is the data for one chunk.
This stuff doesn't work yet or has some caveats:
- I haven't tested resume much.
- Files need an "isPartial()" flag for partial uploads, and the UI needs to respect it.
- The JS client needs to become chunk-aware.
- Chunk size is set crazy low to make testing easier.
- Some debugging flags that I'll remove soon-ish.
- Downloading works, but still streams the whole file into memory.
- This storage engine is disabled by default (hardcoded as a unit test engine) because it's still sketchy.
- Need some code to remove the "isParital" flag when the last chunk is uploaded.
- Maybe do checksumming on chunks.
Test Plan:
- Hacked up `arc upload` (see next diff) to be chunk-aware and uploaded a readme in 18 32-byte chunks. Then downloaded it. Got the same file back that I uploaded.
- File UI now shows some basic chunk info for chunked files:
{F336434}
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: joshuaspence, epriestley
Maniphest Tasks: T7149
Differential Revision: https://secure.phabricator.com/D12060
2015-03-13 19:30:02 +01:00
|
|
|
const METADATA_PARTIAL = 'partial';
|
2013-01-07 18:43:35 +01:00
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
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-10-01 17:45:18 +02:00
|
|
|
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
|
2015-03-13 19:30:24 +01:00
|
|
|
protected $isPartial = 0;
|
2013-02-20 22:33:47 +01:00
|
|
|
|
2013-10-01 17:43:34 +02:00
|
|
|
private $objects = self::ATTACHABLE;
|
|
|
|
private $objectPHIDs = self::ATTACHABLE;
|
2014-09-04 21:49:31 +02:00
|
|
|
private $originalFile = self::ATTACHABLE;
|
|
|
|
|
|
|
|
public static function initializeNewFile() {
|
2014-11-21 20:17:20 +01:00
|
|
|
$app = id(new PhabricatorApplicationQuery())
|
|
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
|
|
|
->withClasses(array('PhabricatorFilesApplication'))
|
|
|
|
->executeOne();
|
|
|
|
|
|
|
|
$view_policy = $app->getPolicy(
|
|
|
|
FilesDefaultViewCapability::CAPABILITY);
|
|
|
|
|
2014-09-04 21:49:31 +02:00
|
|
|
return id(new PhabricatorFile())
|
2014-11-21 20:17:20 +01:00
|
|
|
->setViewPolicy($view_policy)
|
2015-03-13 19:30:24 +01:00
|
|
|
->setIsPartial(0)
|
2014-09-04 21:49:31 +02:00
|
|
|
->attachOriginalFile(null)
|
|
|
|
->attachObjects(array())
|
|
|
|
->attachObjectPHIDs(array());
|
|
|
|
}
|
2013-10-01 17:43:34 +02:00
|
|
|
|
2015-01-13 20:47:05 +01:00
|
|
|
protected function getConfiguration() {
|
2011-01-23 03:33:00 +01:00
|
|
|
return array(
|
|
|
|
self::CONFIG_AUX_PHID => true,
|
2013-01-07 18:43:35 +01:00
|
|
|
self::CONFIG_SERIALIZATION => array(
|
|
|
|
'metadata' => self::SERIALIZATION_JSON,
|
|
|
|
),
|
2014-09-19 14:44:40 +02:00
|
|
|
self::CONFIG_COLUMN_SCHEMA => array(
|
|
|
|
'name' => 'text255?',
|
|
|
|
'mimeType' => 'text255?',
|
2014-10-01 16:53:50 +02:00
|
|
|
'byteSize' => 'uint64',
|
2014-09-19 14:44:40 +02:00
|
|
|
'storageEngine' => 'text32',
|
|
|
|
'storageFormat' => 'text32',
|
|
|
|
'storageHandle' => 'text255',
|
|
|
|
'authorPHID' => 'phid?',
|
|
|
|
'secretKey' => 'bytes20?',
|
|
|
|
'contentHash' => 'bytes40?',
|
|
|
|
'ttl' => 'epoch?',
|
|
|
|
'isExplicitUpload' => 'bool?',
|
|
|
|
'mailKey' => 'bytes20',
|
2015-03-13 19:30:24 +01:00
|
|
|
'isPartial' => 'bool',
|
2014-09-19 14:44:40 +02:00
|
|
|
),
|
|
|
|
self::CONFIG_KEY_SCHEMA => array(
|
|
|
|
'key_phid' => null,
|
|
|
|
'phid' => array(
|
|
|
|
'columns' => array('phid'),
|
2014-10-01 16:53:50 +02:00
|
|
|
'unique' => true,
|
|
|
|
),
|
|
|
|
'authorPHID' => array(
|
|
|
|
'columns' => array('authorPHID'),
|
|
|
|
),
|
|
|
|
'contentHash' => array(
|
|
|
|
'columns' => array('contentHash'),
|
|
|
|
),
|
|
|
|
'key_ttl' => array(
|
|
|
|
'columns' => array('ttl'),
|
|
|
|
),
|
|
|
|
'key_dateCreated' => array(
|
|
|
|
'columns' => array('dateCreated'),
|
2014-09-19 14:44:40 +02:00
|
|
|
),
|
2015-03-13 19:30:24 +01:00
|
|
|
'key_partial' => array(
|
|
|
|
'columns' => array('authorPHID', 'isPartial'),
|
|
|
|
),
|
2014-09-19 14:44:40 +02:00
|
|
|
),
|
2011-01-23 03:33:00 +01:00
|
|
|
) + parent::getConfiguration();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function generatePHID() {
|
2011-03-03 03:58:21 +01:00
|
|
|
return PhabricatorPHID::generateNewPHID(
|
2014-07-24 00:05:46 +02:00
|
|
|
PhabricatorFileFilePHIDType::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();
|
|
|
|
}
|
|
|
|
|
2014-08-02 23:45:50 +02:00
|
|
|
public function getMonogram() {
|
|
|
|
return 'F'.$this->getID();
|
|
|
|
}
|
|
|
|
|
2011-10-19 04:46:52 +02:00
|
|
|
public static function readUploadedFileData($spec) {
|
2011-01-23 03:33:00 +01:00
|
|
|
if (!$spec) {
|
2014-06-09 20:36:49 +02:00
|
|
|
throw new Exception('No file was uploaded!');
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$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) {
|
2014-06-09 20:36:49 +02:00
|
|
|
throw new Exception('File is not an uploaded file.');
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$file_data = Filesystem::readFile($tmp_name);
|
|
|
|
$file_size = idx($spec, 'size');
|
|
|
|
|
|
|
|
if (strlen($file_data) != $file_size) {
|
2014-06-09 20:36:49 +02:00
|
|
|
throw new Exception('File size disagrees with uploaded size.');
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
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()) {
|
|
|
|
return self::newFromFileData($data, $params);
|
|
|
|
}
|
|
|
|
|
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',
|
File names should be editable.
Summary: Fixes T7480, File names should be editable and the event should show up in feed.
Test Plan: Upload a file, view file details, edit file, change file name by adding a space and a word to the name, save changes, file name should retain space and not normalize the name, file details should show the edit event, install feed should correctly show an event for the action.
Reviewers: epriestley, #blessed_reviewers
Reviewed By: epriestley, #blessed_reviewers
Subscribers: Korvin, epriestley
Maniphest Tasks: T7480
Differential Revision: https://secure.phabricator.com/D12561
2015-04-27 00:24:29 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
public static function newFileFromContentHash($hash, array $params) {
|
2013-02-09 16:00:52 +01:00
|
|
|
// Check to see if a file with same contentHash exist
|
|
|
|
$file = id(new PhabricatorFile())->loadOneWhere(
|
2014-08-11 18:39:40 +02:00
|
|
|
'contentHash = %s LIMIT 1',
|
|
|
|
$hash);
|
2013-02-09 16:00:52 +01:00
|
|
|
|
|
|
|
if ($file) {
|
|
|
|
// copy storageEngine, storageHandle, storageFormat
|
|
|
|
$copy_of_storage_engine = $file->getStorageEngine();
|
|
|
|
$copy_of_storage_handle = $file->getStorageHandle();
|
|
|
|
$copy_of_storage_format = $file->getStorageFormat();
|
2015-01-02 23:11:41 +01:00
|
|
|
$copy_of_byte_size = $file->getByteSize();
|
|
|
|
$copy_of_mime_type = $file->getMimeType();
|
2013-02-09 16:00:52 +01:00
|
|
|
|
2014-09-04 21:49:31 +02:00
|
|
|
$new_file = PhabricatorFile::initializeNewFile();
|
2013-02-09 16:00:52 +01:00
|
|
|
|
2015-01-02 23:11:41 +01:00
|
|
|
$new_file->setByteSize($copy_of_byte_size);
|
2013-12-30 20:27:02 +01:00
|
|
|
|
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);
|
2015-01-02 23:11:41 +01:00
|
|
|
$new_file->setMimeType($copy_of_mime_type);
|
2013-03-19 00:31:38 +01:00
|
|
|
$new_file->copyDimensions($file);
|
2013-02-09 16:00:52 +01:00
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
$new_file->readPropertiesFromParameters($params);
|
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
$new_file->save();
|
|
|
|
|
|
|
|
return $new_file;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
Add a chunking storage engine for files
Summary:
Ref T7149. This isn't complete and isn't active yet, but does basically work. I'll shore it up in the next few diffs.
The new workflow goes like this:
> Client, file.allocate(): I'd like to upload a file with length L, metadata M, and hash H.
Then the server returns `upload` (a boolean) and `filePHID` (a PHID). These mean:
| upload | filePHID | means |
|---|---|---|
| false | false | Server can't accept file.
| false | true | File data already known, file created from hash.
| true | false | Just upload normally.
| true | true | Query chunks to start or resume a chunked upload.
All but the last case are uninteresting and work like exising uploads with `file.uploadhash` (which we can eventually deprecate).
In the last case:
> Client, file.querychunks(): Give me a list of chunks that I should upload.
This returns all the chunks for the file. Chunks have a start byte, an end byte, and a "complete" flag to indicate that the server already has the data.
Then, the client fills in chunks by sending them:
> Client, file.uploadchunk(): Here is the data for one chunk.
This stuff doesn't work yet or has some caveats:
- I haven't tested resume much.
- Files need an "isPartial()" flag for partial uploads, and the UI needs to respect it.
- The JS client needs to become chunk-aware.
- Chunk size is set crazy low to make testing easier.
- Some debugging flags that I'll remove soon-ish.
- Downloading works, but still streams the whole file into memory.
- This storage engine is disabled by default (hardcoded as a unit test engine) because it's still sketchy.
- Need some code to remove the "isParital" flag when the last chunk is uploaded.
- Maybe do checksumming on chunks.
Test Plan:
- Hacked up `arc upload` (see next diff) to be chunk-aware and uploaded a readme in 18 32-byte chunks. Then downloaded it. Got the same file back that I uploaded.
- File UI now shows some basic chunk info for chunked files:
{F336434}
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: joshuaspence, epriestley
Maniphest Tasks: T7149
Differential Revision: https://secure.phabricator.com/D12060
2015-03-13 19:30:02 +01:00
|
|
|
public static function newChunkedFile(
|
|
|
|
PhabricatorFileStorageEngine $engine,
|
|
|
|
$length,
|
|
|
|
array $params) {
|
|
|
|
|
|
|
|
$file = PhabricatorFile::initializeNewFile();
|
|
|
|
|
|
|
|
$file->setByteSize($length);
|
|
|
|
|
|
|
|
// TODO: We might be able to test the first chunk in order to figure
|
|
|
|
// this out more reliably, since MIME detection usually examines headers.
|
|
|
|
// However, enormous files are probably always either actually raw data
|
|
|
|
// or reasonable to treat like raw data.
|
|
|
|
$file->setMimeType('application/octet-stream');
|
|
|
|
|
|
|
|
$chunked_hash = idx($params, 'chunkedHash');
|
|
|
|
if ($chunked_hash) {
|
|
|
|
$file->setContentHash($chunked_hash);
|
|
|
|
} else {
|
|
|
|
// See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
|
|
|
|
// discussion of this.
|
2015-03-14 16:28:46 +01:00
|
|
|
$seed = Filesystem::readRandomBytes(64);
|
|
|
|
$hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
|
|
|
|
$seed);
|
|
|
|
$file->setContentHash($hash);
|
Add a chunking storage engine for files
Summary:
Ref T7149. This isn't complete and isn't active yet, but does basically work. I'll shore it up in the next few diffs.
The new workflow goes like this:
> Client, file.allocate(): I'd like to upload a file with length L, metadata M, and hash H.
Then the server returns `upload` (a boolean) and `filePHID` (a PHID). These mean:
| upload | filePHID | means |
|---|---|---|
| false | false | Server can't accept file.
| false | true | File data already known, file created from hash.
| true | false | Just upload normally.
| true | true | Query chunks to start or resume a chunked upload.
All but the last case are uninteresting and work like exising uploads with `file.uploadhash` (which we can eventually deprecate).
In the last case:
> Client, file.querychunks(): Give me a list of chunks that I should upload.
This returns all the chunks for the file. Chunks have a start byte, an end byte, and a "complete" flag to indicate that the server already has the data.
Then, the client fills in chunks by sending them:
> Client, file.uploadchunk(): Here is the data for one chunk.
This stuff doesn't work yet or has some caveats:
- I haven't tested resume much.
- Files need an "isPartial()" flag for partial uploads, and the UI needs to respect it.
- The JS client needs to become chunk-aware.
- Chunk size is set crazy low to make testing easier.
- Some debugging flags that I'll remove soon-ish.
- Downloading works, but still streams the whole file into memory.
- This storage engine is disabled by default (hardcoded as a unit test engine) because it's still sketchy.
- Need some code to remove the "isParital" flag when the last chunk is uploaded.
- Maybe do checksumming on chunks.
Test Plan:
- Hacked up `arc upload` (see next diff) to be chunk-aware and uploaded a readme in 18 32-byte chunks. Then downloaded it. Got the same file back that I uploaded.
- File UI now shows some basic chunk info for chunked files:
{F336434}
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: joshuaspence, epriestley
Maniphest Tasks: T7149
Differential Revision: https://secure.phabricator.com/D12060
2015-03-13 19:30:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$file->setStorageEngine($engine->getEngineIdentifier());
|
|
|
|
$file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
|
|
|
|
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
|
2015-03-13 19:30:24 +01:00
|
|
|
$file->setIsPartial(1);
|
Add a chunking storage engine for files
Summary:
Ref T7149. This isn't complete and isn't active yet, but does basically work. I'll shore it up in the next few diffs.
The new workflow goes like this:
> Client, file.allocate(): I'd like to upload a file with length L, metadata M, and hash H.
Then the server returns `upload` (a boolean) and `filePHID` (a PHID). These mean:
| upload | filePHID | means |
|---|---|---|
| false | false | Server can't accept file.
| false | true | File data already known, file created from hash.
| true | false | Just upload normally.
| true | true | Query chunks to start or resume a chunked upload.
All but the last case are uninteresting and work like exising uploads with `file.uploadhash` (which we can eventually deprecate).
In the last case:
> Client, file.querychunks(): Give me a list of chunks that I should upload.
This returns all the chunks for the file. Chunks have a start byte, an end byte, and a "complete" flag to indicate that the server already has the data.
Then, the client fills in chunks by sending them:
> Client, file.uploadchunk(): Here is the data for one chunk.
This stuff doesn't work yet or has some caveats:
- I haven't tested resume much.
- Files need an "isPartial()" flag for partial uploads, and the UI needs to respect it.
- The JS client needs to become chunk-aware.
- Chunk size is set crazy low to make testing easier.
- Some debugging flags that I'll remove soon-ish.
- Downloading works, but still streams the whole file into memory.
- This storage engine is disabled by default (hardcoded as a unit test engine) because it's still sketchy.
- Need some code to remove the "isParital" flag when the last chunk is uploaded.
- Maybe do checksumming on chunks.
Test Plan:
- Hacked up `arc upload` (see next diff) to be chunk-aware and uploaded a readme in 18 32-byte chunks. Then downloaded it. Got the same file back that I uploaded.
- File UI now shows some basic chunk info for chunked files:
{F336434}
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: joshuaspence, epriestley
Maniphest Tasks: T7149
Differential Revision: https://secure.phabricator.com/D12060
2015-03-13 19:30:02 +01:00
|
|
|
|
|
|
|
$file->readPropertiesFromParameters($params);
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
2013-02-09 16:00:52 +01:00
|
|
|
private static function buildFromFileData($data, array $params = array()) {
|
2011-07-20 07:48:38 +02:00
|
|
|
|
2013-02-06 22:37:42 +01:00
|
|
|
if (isset($params['storageEngines'])) {
|
|
|
|
$engines = $params['storageEngines'];
|
|
|
|
} else {
|
2015-03-12 21:28:53 +01:00
|
|
|
$size = strlen($data);
|
|
|
|
$engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
|
|
|
|
|
|
|
|
if (!$engines) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'No configured storage engine can store this file. See '.
|
|
|
|
'"Configuring File Storage" in the documentation for '.
|
|
|
|
'information on configuring storage engines.'));
|
|
|
|
}
|
2013-02-06 22:37:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
|
2011-07-20 07:48:38 +02:00
|
|
|
if (!$engines) {
|
2015-03-12 21:28:53 +01:00
|
|
|
throw new Exception(pht('No valid storage engines are available!'));
|
2011-07-20 07:48:38 +02:00
|
|
|
}
|
|
|
|
|
2014-09-04 21:49:31 +02:00
|
|
|
$file = PhabricatorFile::initializeNewFile();
|
2012-10-25 20:36:38 +02:00
|
|
|
|
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(
|
2014-06-09 20:36:49 +02:00
|
|
|
'All storage engines failed to write file:',
|
2012-04-09 00:07:34 +02:00
|
|
|
$exceptions);
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$file->setByteSize(strlen($data));
|
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);
|
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
$file->readPropertiesFromParameters($params);
|
|
|
|
|
|
|
|
if (!$file->getMimeType()) {
|
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();
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
$old_identifier = $this->getStorageEngine();
|
2012-10-25 20:36:38 +02:00
|
|
|
$old_handle = $this->getStorageHandle();
|
|
|
|
|
|
|
|
$this->setStorageEngine($new_identifier);
|
|
|
|
$this->setStorageHandle($new_handle);
|
|
|
|
$this->save();
|
|
|
|
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
$this->deleteFileDataIfUnused(
|
|
|
|
$old_engine,
|
|
|
|
$old_identifier,
|
|
|
|
$old_handle);
|
2012-10-25 20:36:38 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-03-25 02:49:01 +01:00
|
|
|
/**
|
|
|
|
* Download a remote resource over HTTP and save the response body as a file.
|
|
|
|
*
|
|
|
|
* This method respects `security.outbound-blacklist`, and protects against
|
|
|
|
* HTTP redirection (by manually following "Location" headers and verifying
|
|
|
|
* each destination). It does not protect against DNS rebinding. See
|
|
|
|
* discussion in T6755.
|
|
|
|
*/
|
2013-02-21 21:43:39 +01:00
|
|
|
public static function newFromFileDownload($uri, array $params = array()) {
|
2012-06-19 00:11:47 +02:00
|
|
|
$timeout = 5;
|
2013-02-21 21:43:39 +01:00
|
|
|
|
2015-03-25 02:49:01 +01:00
|
|
|
$redirects = array();
|
|
|
|
$current = $uri;
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
if (count($redirects) > 10) {
|
|
|
|
throw new Exception(
|
|
|
|
pht('Too many redirects trying to fetch remote URI.'));
|
|
|
|
}
|
|
|
|
|
2015-03-26 19:12:22 +01:00
|
|
|
$resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
|
2015-03-25 02:49:01 +01:00
|
|
|
$current,
|
|
|
|
array(
|
|
|
|
'http',
|
|
|
|
'https',
|
|
|
|
));
|
|
|
|
|
2015-03-26 19:12:22 +01:00
|
|
|
list($resolved_uri, $resolved_domain) = $resolved;
|
|
|
|
|
|
|
|
$current = new PhutilURI($current);
|
|
|
|
if ($current->getProtocol() == 'http') {
|
|
|
|
// For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
|
|
|
|
$fetch_uri = $resolved_uri;
|
|
|
|
$fetch_host = $resolved_domain;
|
|
|
|
} else {
|
|
|
|
// For HTTPS, we can't: cURL won't verify the SSL certificate if
|
|
|
|
// the domain has been replaced with an IP. But internal services
|
|
|
|
// presumably will not have valid certificates for rebindable
|
|
|
|
// domain names on attacker-controlled domains, so the DNS rebinding
|
|
|
|
// attack should generally not be possible anyway.
|
|
|
|
$fetch_uri = $current;
|
|
|
|
$fetch_host = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$future = id(new HTTPSFuture($fetch_uri))
|
2015-03-25 02:49:01 +01:00
|
|
|
->setFollowLocation(false)
|
2015-03-26 19:12:22 +01:00
|
|
|
->setTimeout($timeout);
|
|
|
|
|
|
|
|
if ($fetch_host !== null) {
|
|
|
|
$future->addHeader('Host', $fetch_host);
|
|
|
|
}
|
|
|
|
|
|
|
|
list($status, $body, $headers) = $future->resolve();
|
2015-03-25 02:49:01 +01:00
|
|
|
|
|
|
|
if ($status->isRedirect()) {
|
|
|
|
// This is an HTTP 3XX status, so look for a "Location" header.
|
|
|
|
$location = null;
|
|
|
|
foreach ($headers as $header) {
|
|
|
|
list($name, $value) = $header;
|
|
|
|
if (phutil_utf8_strtolower($name) == 'location') {
|
|
|
|
$location = $value;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTP 3XX status with no "Location" header, just treat this like
|
|
|
|
// a normal HTTP error.
|
|
|
|
if ($location === null) {
|
|
|
|
throw $status;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($redirects[$location])) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'Encountered loop while following redirects.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
$redirects[$location] = $location;
|
|
|
|
$current = $location;
|
|
|
|
// We'll fall off the bottom and go try this URI now.
|
|
|
|
} else if ($status->isError()) {
|
|
|
|
// This is something other than an HTTP 2XX or HTTP 3XX status, so
|
|
|
|
// just bail out.
|
|
|
|
throw $status;
|
|
|
|
} else {
|
|
|
|
// This is HTTP 2XX, so use the the response body to save the
|
|
|
|
// file data.
|
|
|
|
$params = $params + array(
|
|
|
|
'name' => basename($uri),
|
|
|
|
);
|
|
|
|
|
|
|
|
return self::newFromFileData($body, $params);
|
|
|
|
}
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
if ($redirects) {
|
|
|
|
throw new PhutilProxyException(
|
|
|
|
pht(
|
|
|
|
'Failed to fetch remote URI "%s" after following %s redirect(s) '.
|
|
|
|
'(%s): %s',
|
|
|
|
$uri,
|
|
|
|
new PhutilNumber(count($redirects)),
|
|
|
|
implode(' > ', array_keys($redirects)),
|
|
|
|
$ex->getMessage()),
|
|
|
|
$ex);
|
|
|
|
} else {
|
|
|
|
throw $ex;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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() {
|
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();
|
|
|
|
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
$this->deleteFileDataIfUnused(
|
|
|
|
$this->instantiateStorageEngine(),
|
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
|
|
|
$this->getStorageEngine(),
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
$this->getStorageHandle());
|
2013-02-09 16:00:52 +01:00
|
|
|
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroy stored file data if there are no remaining files which reference
|
|
|
|
* it.
|
|
|
|
*/
|
2014-08-21 20:47:59 +02:00
|
|
|
public function deleteFileDataIfUnused(
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
PhabricatorFileStorageEngine $engine,
|
|
|
|
$engine_identifier,
|
|
|
|
$handle) {
|
|
|
|
|
|
|
|
// Check to see if any files are using storage.
|
|
|
|
$usage = id(new PhabricatorFile())->loadAllWhere(
|
|
|
|
'storageEngine = %s AND storageHandle = %s LIMIT 1',
|
|
|
|
$engine_identifier,
|
|
|
|
$handle);
|
|
|
|
|
|
|
|
// If there are no files using the storage, destroy the actual storage.
|
|
|
|
if (!$usage) {
|
2013-05-29 15:28:57 +02:00
|
|
|
try {
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
$engine->deleteFile($handle);
|
2013-05-29 15:28:57 +02:00
|
|
|
} catch (Exception $ex) {
|
|
|
|
// In the worst case, we're leaving some data stranded in a storage
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
// engine, which is not a big deal.
|
2013-05-29 15:28:57 +02:00
|
|
|
phlog($ex);
|
|
|
|
}
|
2013-02-09 16:00:52 +01:00
|
|
|
}
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
Fix an issue where migrating files could prematurely destroy duplicates
Summary:
Fixes T5912. When migrating files, we try to clean up the old data. However, this code isn't aware of reference counting, and unconditionally destroys the old data.
For example, if you migrate files `F1` and `F2` and they have the same data, we'll delete the shared data when we migrate `F1`. Then you'll get an error when you migrate `F2`.
Since this only affects duplicate files, it primarily hits default profile pictures, which are the most numerous duplicate files on most installs.
Test Plan:
- Verified that the theory was correct by uploading two copies of a file and migrating the first one, before applying the patch. The second one's data was nuked and it couldn't be migrated.
- Applied patch.
- Uploaded two copies of a new file, migrated the first one (no data deletion), migrated the second one (data correctly deleted).
- Uploaded two copies of another new file, `bin/remove destory'd` the first one (no data deletion), then did it to the second one (data correctly deleted).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5912
Differential Revision: https://secure.phabricator.com/D10312
2014-08-21 00:32:32 +02:00
|
|
|
|
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:
|
2014-06-09 20:36:49 +02:00
|
|
|
throw new Exception('Unknown storage format.');
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
}
|
|
|
|
|
2015-03-14 16:28:59 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an iterable which emits file content bytes.
|
|
|
|
*
|
|
|
|
* @param int Offset for the start of data.
|
|
|
|
* @param int Offset for the end of data.
|
|
|
|
* @return Iterable Iterable object which emits requested data.
|
|
|
|
*/
|
|
|
|
public function getFileDataIterator($begin = null, $end = null) {
|
2015-03-14 16:29:30 +01:00
|
|
|
$engine = $this->instantiateStorageEngine();
|
|
|
|
return $engine->getFileDataIterator($this, $begin, $end);
|
2015-03-14 16:28:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
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(
|
2014-06-09 20:36:49 +02:00
|
|
|
'You must save a file before you can generate a view URI.');
|
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
|
|
|
}
|
|
|
|
|
When an install is instanced, include the instance identifier in the URI for file data
Summary:
This allows us to CDN the cluster.
General problem is that we can't easily give each instance its own CDN URI (`giraffe.phcdn.net`) in Cloudfront, because it requires that you enumerate all aliases (and there's a limit of 100) and depends on SNI (a newish feature of SSL which allows one server to serve multiple certificates, but which doesn't have full support everywhere yet).
It's //possible// that we could eventually work around this, or use Cloudflare instead (which has a different model that seems like a slightly easier fit for CDN-domain-per-instance), but I don't want to sink a ton of work into this and want to keep things on AWS insofar as we reasonably can.
The easiest way to fix this is just to put the instance identity into URIs, then read it out when handling CDN requests. This has no effect on installs without cluster instance configuration, which is all of them except ours.
It's also slightly desirable to share this stuff, since we get to share the cache for static resources, which are always identical across instances.
So requests go from the Cloudfront gateway ("xyz.cloudfront.com") to the LB with a hard-coded instance name ("cdn.phacility.com"), which gets them routed to a balanced web machine. The web machine picks the correct instance name out of the URI, acts as that instance, and does the correct thing.
The messiest part of this is that we need "cdn.phacility.com" to be a real instance so it can serve static resources, but that's not a big deal. We have a few other hard-codes which have to be real resources for now, like we must have a merchant named "Phacility".
Test Plan:
- Viewed files with `security.alternate-file-domain` off (i.e., no file tokens).
- Viewed pages and files with `security.alternate-file-domain` on. Saw correct resource behavior, @isntance generation of URIs, and correct token redirect behavior for files.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Differential Revision: https://secure.phabricator.com/D11668
2015-02-03 23:55:46 +01:00
|
|
|
return $this->getCDNURI(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getCDNURI($token) {
|
File names should be editable.
Summary: Fixes T7480, File names should be editable and the event should show up in feed.
Test Plan: Upload a file, view file details, edit file, change file name by adding a space and a word to the name, save changes, file name should retain space and not normalize the name, file details should show the edit event, install feed should correctly show an event for the action.
Reviewers: epriestley, #blessed_reviewers
Reviewed By: epriestley, #blessed_reviewers
Subscribers: Korvin, epriestley
Maniphest Tasks: T7480
Differential Revision: https://secure.phabricator.com/D12561
2015-04-27 00:24:29 +02:00
|
|
|
$name = self::normalizeFileName($this->getName());
|
|
|
|
$name = phutil_escape_uri($name);
|
2012-01-17 01:20:18 +01:00
|
|
|
|
When an install is instanced, include the instance identifier in the URI for file data
Summary:
This allows us to CDN the cluster.
General problem is that we can't easily give each instance its own CDN URI (`giraffe.phcdn.net`) in Cloudfront, because it requires that you enumerate all aliases (and there's a limit of 100) and depends on SNI (a newish feature of SSL which allows one server to serve multiple certificates, but which doesn't have full support everywhere yet).
It's //possible// that we could eventually work around this, or use Cloudflare instead (which has a different model that seems like a slightly easier fit for CDN-domain-per-instance), but I don't want to sink a ton of work into this and want to keep things on AWS insofar as we reasonably can.
The easiest way to fix this is just to put the instance identity into URIs, then read it out when handling CDN requests. This has no effect on installs without cluster instance configuration, which is all of them except ours.
It's also slightly desirable to share this stuff, since we get to share the cache for static resources, which are always identical across instances.
So requests go from the Cloudfront gateway ("xyz.cloudfront.com") to the LB with a hard-coded instance name ("cdn.phacility.com"), which gets them routed to a balanced web machine. The web machine picks the correct instance name out of the URI, acts as that instance, and does the correct thing.
The messiest part of this is that we need "cdn.phacility.com" to be a real instance so it can serve static resources, but that's not a big deal. We have a few other hard-codes which have to be real resources for now, like we must have a merchant named "Phacility".
Test Plan:
- Viewed files with `security.alternate-file-domain` off (i.e., no file tokens).
- Viewed pages and files with `security.alternate-file-domain` on. Saw correct resource behavior, @isntance generation of URIs, and correct token redirect behavior for files.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Differential Revision: https://secure.phabricator.com/D11668
2015-02-03 23:55:46 +01:00
|
|
|
$parts = array();
|
|
|
|
$parts[] = 'file';
|
|
|
|
$parts[] = 'data';
|
|
|
|
|
|
|
|
// If this is an instanced install, add the instance identifier to the URI.
|
|
|
|
// Instanced configurations behind a CDN may not be able to control the
|
|
|
|
// request domain used by the CDN (as with AWS CloudFront). Embedding the
|
|
|
|
// instance identity in the path allows us to distinguish between requests
|
|
|
|
// originating from different instances but served through the same CDN.
|
|
|
|
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
|
|
|
|
if (strlen($instance)) {
|
|
|
|
$parts[] = '@'.$instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
$parts[] = $this->getSecretKey();
|
|
|
|
$parts[] = $this->getPHID();
|
|
|
|
if ($token) {
|
|
|
|
$parts[] = $token;
|
|
|
|
}
|
|
|
|
$parts[] = $name;
|
|
|
|
|
2015-03-13 19:30:24 +01:00
|
|
|
$path = '/'.implode('/', $parts);
|
When an install is instanced, include the instance identifier in the URI for file data
Summary:
This allows us to CDN the cluster.
General problem is that we can't easily give each instance its own CDN URI (`giraffe.phcdn.net`) in Cloudfront, because it requires that you enumerate all aliases (and there's a limit of 100) and depends on SNI (a newish feature of SSL which allows one server to serve multiple certificates, but which doesn't have full support everywhere yet).
It's //possible// that we could eventually work around this, or use Cloudflare instead (which has a different model that seems like a slightly easier fit for CDN-domain-per-instance), but I don't want to sink a ton of work into this and want to keep things on AWS insofar as we reasonably can.
The easiest way to fix this is just to put the instance identity into URIs, then read it out when handling CDN requests. This has no effect on installs without cluster instance configuration, which is all of them except ours.
It's also slightly desirable to share this stuff, since we get to share the cache for static resources, which are always identical across instances.
So requests go from the Cloudfront gateway ("xyz.cloudfront.com") to the LB with a hard-coded instance name ("cdn.phacility.com"), which gets them routed to a balanced web machine. The web machine picks the correct instance name out of the URI, acts as that instance, and does the correct thing.
The messiest part of this is that we need "cdn.phacility.com" to be a real instance so it can serve static resources, but that's not a big deal. We have a few other hard-codes which have to be real resources for now, like we must have a merchant named "Phacility".
Test Plan:
- Viewed files with `security.alternate-file-domain` off (i.e., no file tokens).
- Viewed pages and files with `security.alternate-file-domain` on. Saw correct resource behavior, @isntance generation of URIs, and correct token redirect behavior for files.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Differential Revision: https://secure.phabricator.com/D11668
2015-02-03 23:55:46 +01:00
|
|
|
|
2015-03-13 19:30:24 +01:00
|
|
|
// If this file is only partially uploaded, we're just going to return a
|
|
|
|
// local URI to make sure that Ajax works, since the page is inevitably
|
|
|
|
// going to give us an error back.
|
|
|
|
if ($this->getIsPartial()) {
|
|
|
|
return PhabricatorEnv::getURI($path);
|
|
|
|
} else {
|
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
|
|
|
}
|
2011-01-26 18:02:09 +01:00
|
|
|
}
|
2011-02-22 18:22:57 +01:00
|
|
|
|
When an install is instanced, include the instance identifier in the URI for file data
Summary:
This allows us to CDN the cluster.
General problem is that we can't easily give each instance its own CDN URI (`giraffe.phcdn.net`) in Cloudfront, because it requires that you enumerate all aliases (and there's a limit of 100) and depends on SNI (a newish feature of SSL which allows one server to serve multiple certificates, but which doesn't have full support everywhere yet).
It's //possible// that we could eventually work around this, or use Cloudflare instead (which has a different model that seems like a slightly easier fit for CDN-domain-per-instance), but I don't want to sink a ton of work into this and want to keep things on AWS insofar as we reasonably can.
The easiest way to fix this is just to put the instance identity into URIs, then read it out when handling CDN requests. This has no effect on installs without cluster instance configuration, which is all of them except ours.
It's also slightly desirable to share this stuff, since we get to share the cache for static resources, which are always identical across instances.
So requests go from the Cloudfront gateway ("xyz.cloudfront.com") to the LB with a hard-coded instance name ("cdn.phacility.com"), which gets them routed to a balanced web machine. The web machine picks the correct instance name out of the URI, acts as that instance, and does the correct thing.
The messiest part of this is that we need "cdn.phacility.com" to be a real instance so it can serve static resources, but that's not a big deal. We have a few other hard-codes which have to be real resources for now, like we must have a merchant named "Phacility".
Test Plan:
- Viewed files with `security.alternate-file-domain` off (i.e., no file tokens).
- Viewed pages and files with `security.alternate-file-domain` on. Saw correct resource behavior, @isntance generation of URIs, and correct token redirect behavior for files.
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Differential Revision: https://secure.phabricator.com/D11668
2015-02-03 23:55:46 +01:00
|
|
|
/**
|
|
|
|
* Get the CDN URI for this file, including a one-time-use security token.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public function getCDNURIWithToken() {
|
|
|
|
if (!$this->getPHID()) {
|
|
|
|
throw new Exception(
|
|
|
|
'You must save a file before you can generate a CDN URI.');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->getCDNURI($this->generateOneTimeToken());
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-07-29 19:00:16 +02:00
|
|
|
public function getInfoURI() {
|
2014-08-20 22:18:21 +02:00
|
|
|
return '/'.$this->getMonogram();
|
2011-07-29 19:00:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-02-10 00:31:47 +01:00
|
|
|
private function getTransformedURI($transform) {
|
|
|
|
$parts = array();
|
|
|
|
$parts[] = 'file';
|
|
|
|
$parts[] = 'xform';
|
|
|
|
|
|
|
|
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
|
|
|
|
if (strlen($instance)) {
|
|
|
|
$parts[] = '@'.$instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
$parts[] = $transform;
|
|
|
|
$parts[] = $this->getPHID();
|
|
|
|
$parts[] = $this->getSecretKey();
|
|
|
|
|
|
|
|
$path = implode('/', $parts);
|
|
|
|
$path = $path.'/';
|
|
|
|
|
2013-06-17 16:08:50 +02:00
|
|
|
return PhabricatorEnv::getCDNURI($path);
|
|
|
|
}
|
|
|
|
|
2015-02-10 00:31:47 +01:00
|
|
|
public function getProfileThumbURI() {
|
|
|
|
return $this->getTransformedURI('thumb-profile');
|
|
|
|
}
|
|
|
|
|
2014-06-15 06:12:19 +02:00
|
|
|
public function getPreview100URI() {
|
2015-02-10 00:31:47 +01:00
|
|
|
return $this->getTransformedURI('preview-100');
|
2014-06-15 06:12:19 +02:00
|
|
|
}
|
|
|
|
|
2012-10-08 22:26:10 +02:00
|
|
|
public function getPreview220URI() {
|
2015-02-10 00:31:47 +01:00
|
|
|
return $this->getTransformedURI('preview-220');
|
2012-10-08 22:26:10 +02:00
|
|
|
}
|
|
|
|
|
2013-03-05 17:05:51 +01:00
|
|
|
public function getThumb280x210URI() {
|
2015-02-10 00:31:47 +01:00
|
|
|
return $this->getTransformedURI('thumb-280x210');
|
2013-03-05 17:05:51 +01:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2013-09-27 19:51:25 +02:00
|
|
|
public function isAudio() {
|
|
|
|
if (!$this->isViewableInBrowser()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-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;
|
|
|
|
}
|
|
|
|
|
2014-08-21 20:47:59 +02:00
|
|
|
public 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();
|
2014-12-21 05:59:24 +01:00
|
|
|
return idx($mime_map, $mime_type, 'fa-file-o');
|
2013-03-16 07:41:36 +01:00
|
|
|
}
|
|
|
|
|
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(
|
2014-06-09 20:36:49 +02:00
|
|
|
'This file is not a viewable image.');
|
2013-01-07 18:43:35 +01:00
|
|
|
}
|
|
|
|
|
2014-06-09 20:36:49 +02:00
|
|
|
if (!function_exists('imagecreatefromstring')) {
|
2013-01-07 18:43:35 +01:00
|
|
|
throw new Exception(
|
2014-06-09 20:36:49 +02:00
|
|
|
'Cannot retrieve image information.');
|
2013-01-07 18:43:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$data = $this->loadFileData();
|
|
|
|
|
|
|
|
$img = imagecreatefromstring($data);
|
|
|
|
if ($img === false) {
|
|
|
|
throw new Exception(
|
2014-06-09 20:36:49 +02:00
|
|
|
'Error when decoding image.');
|
2013-01-07 18:43:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
Fix some file policy issues and add a "Query Workspace"
Summary:
Ref T603. Several issues here:
1. Currently, `FileQuery` does not actually respect object attachment edges when doing policy checks. Everything else works fine, but this was missing an `array_keys()`.
2. Once that's fixed, we hit a bunch of recursion issues. For example, when loading a User we load the profile picture, and then that loads the User, and that loads the profile picture, etc.
3. Introduce a "Query Workspace", which holds objects we know we've loaded and know we can see but haven't finished filtering and/or attaching data to. This allows subqueries to look up objects instead of querying for them.
- We can probably generalize this a bit to make a few other queries more efficient. Pholio currently has a similar (but less general) "mock cache". However, it's keyed by ID instead of PHID so it's not easy to reuse this right now.
This is a bit complex for the problem being solved, but I think it's the cleanest approach and I believe the primitive will be useful in the future.
Test Plan: Looked at pastes, macros, mocks and projects as a logged-in and logged-out user.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D7309
2013-10-14 23:36:06 +02:00
|
|
|
// NOTE: Anyone is allowed to access builtin files.
|
|
|
|
|
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
|
|
|
$files = id(new PhabricatorFileQuery())
|
Fix some file policy issues and add a "Query Workspace"
Summary:
Ref T603. Several issues here:
1. Currently, `FileQuery` does not actually respect object attachment edges when doing policy checks. Everything else works fine, but this was missing an `array_keys()`.
2. Once that's fixed, we hit a bunch of recursion issues. For example, when loading a User we load the profile picture, and then that loads the User, and that loads the profile picture, etc.
3. Introduce a "Query Workspace", which holds objects we know we've loaded and know we can see but haven't finished filtering and/or attaching data to. This allows subqueries to look up objects instead of querying for them.
- We can probably generalize this a bit to make a few other queries more efficient. Pholio currently has a similar (but less general) "mock cache". However, it's keyed by ID instead of PHID so it's not easy to reuse this right now.
This is a bit complex for the problem being solved, but I think it's the cleanest approach and I believe the primitive will be useful in the future.
Test Plan: Looked at pastes, macros, mocks and projects as a logged-in and logged-out user.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D7309
2013-10-14 23:36:06 +02:00
|
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
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
|
|
|
->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),
|
2015-03-01 21:12:38 +01:00
|
|
|
'canCDN' => true,
|
|
|
|
'builtin' => $name,
|
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
|
|
|
);
|
|
|
|
|
|
|
|
$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);
|
|
|
|
|
2013-10-01 17:43:34 +02:00
|
|
|
$file->attachObjectPHIDs(array());
|
|
|
|
$file->attachObjects(array());
|
|
|
|
|
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
|
|
|
$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);
|
|
|
|
}
|
|
|
|
|
2013-10-01 17:43:34 +02:00
|
|
|
public function getObjects() {
|
|
|
|
return $this->assertAttached($this->objects);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function attachObjects(array $objects) {
|
|
|
|
$this->objects = $objects;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getObjectPHIDs() {
|
|
|
|
return $this->assertAttached($this->objectPHIDs);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function attachObjectPHIDs(array $object_phids) {
|
|
|
|
$this->objectPHIDs = $object_phids;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-09-04 21:49:31 +02:00
|
|
|
public function getOriginalFile() {
|
|
|
|
return $this->assertAttached($this->originalFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function attachOriginalFile(PhabricatorFile $file = null) {
|
|
|
|
$this->originalFile = $file;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
Modernize file embed Remarkup rule
Summary: Ref T603. Make this rule properly policy-aware, and extend from `PhabricatorRemarkupRuleObject`.
Test Plan:
- Embedded an image, tested all options (name, link, float, layout, size).
- Used lightbox to view several images.
- Embedded a text file, tested all options (name).
- Embedded audio, tested all options (loop, autoplay).
- Attached a file via comment to a task, verified edge was created.
- Attached a file via comment to a conpherence, verified edge was created.
- Viewed old files, verified remarkup version bump rendered them correctly.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T603
Differential Revision: https://secure.phabricator.com/D7192
2013-10-02 03:03:09 +02:00
|
|
|
public function getImageHeight() {
|
|
|
|
if (!$this->isViewableImage()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getImageWidth() {
|
|
|
|
if (!$this->isViewableImage()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
|
|
|
|
}
|
|
|
|
|
2014-08-08 03:56:19 +02:00
|
|
|
public function getCanCDN() {
|
|
|
|
if (!$this->isViewableImage()) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-08-14 21:13:26 +02:00
|
|
|
|
2014-08-08 03:56:19 +02:00
|
|
|
return idx($this->metadata, self::METADATA_CAN_CDN);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setCanCDN($can_cdn) {
|
|
|
|
$this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-03-01 21:12:38 +01:00
|
|
|
public function isBuiltin() {
|
|
|
|
return ($this->getBuiltinName() !== null);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getBuiltinName() {
|
|
|
|
return idx($this->metadata, self::METADATA_BUILTIN);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setBuiltinName($name) {
|
|
|
|
$this->metadata[self::METADATA_BUILTIN] = $name;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-08-08 03:56:19 +02:00
|
|
|
protected function generateOneTimeToken() {
|
|
|
|
$key = Filesystem::readRandomCharacters(16);
|
|
|
|
|
|
|
|
// Save the new secret.
|
2014-08-11 16:08:58 +02:00
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
$token = id(new PhabricatorAuthTemporaryToken())
|
|
|
|
->setObjectPHID($this->getPHID())
|
|
|
|
->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
|
|
|
|
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
|
|
|
|
->setTokenCode(PhabricatorHash::digest($key))
|
|
|
|
->save();
|
|
|
|
unset($unguarded);
|
|
|
|
|
|
|
|
return $key;
|
2014-08-08 03:56:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function validateOneTimeToken($token_code) {
|
|
|
|
$token = id(new PhabricatorAuthTemporaryTokenQuery())
|
|
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
|
|
|
->withObjectPHIDs(array($this->getPHID()))
|
|
|
|
->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
|
|
|
|
->withExpired(false)
|
2014-08-11 16:08:58 +02:00
|
|
|
->withTokenCodes(array(PhabricatorHash::digest($token_code)))
|
2014-08-08 03:56:19 +02:00
|
|
|
->executeOne();
|
|
|
|
|
|
|
|
return $token;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-10-07 02:07:55 +02:00
|
|
|
/**
|
|
|
|
* Write the policy edge between this file and some object.
|
|
|
|
*
|
|
|
|
* @param phid Object PHID to attach to.
|
|
|
|
* @return this
|
|
|
|
*/
|
2014-09-04 21:51:33 +02:00
|
|
|
public function attachToObject($phid) {
|
2015-01-03 00:33:25 +01:00
|
|
|
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
|
2013-10-07 02:07:55 +02:00
|
|
|
|
|
|
|
id(new PhabricatorEdgeEditor())
|
|
|
|
->addEdge($phid, $edge_type, $this->getPHID())
|
|
|
|
->save();
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2014-09-04 21:49:31 +02:00
|
|
|
/**
|
|
|
|
* Remove the policy edge between this file and some object.
|
|
|
|
*
|
|
|
|
* @param phid Object PHID to detach from.
|
|
|
|
* @return this
|
|
|
|
*/
|
|
|
|
public function detachFromObject($phid) {
|
2015-01-03 00:33:25 +01:00
|
|
|
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
|
2014-09-04 21:49:31 +02:00
|
|
|
|
|
|
|
id(new PhabricatorEdgeEditor())
|
|
|
|
->removeEdge($phid, $edge_type, $this->getPHID())
|
|
|
|
->save();
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
/**
|
|
|
|
* Configure a newly created file object according to specified parameters.
|
|
|
|
*
|
|
|
|
* This method is called both when creating a file from fresh data, and
|
|
|
|
* when creating a new file which reuses existing storage.
|
|
|
|
*
|
|
|
|
* @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile}
|
|
|
|
* for documentation.
|
|
|
|
* @return this
|
|
|
|
*/
|
|
|
|
private function readPropertiesFromParameters(array $params) {
|
|
|
|
$file_name = idx($params, 'name');
|
|
|
|
$this->setName($file_name);
|
|
|
|
|
|
|
|
$author_phid = idx($params, 'authorPHID');
|
|
|
|
$this->setAuthorPHID($author_phid);
|
|
|
|
|
|
|
|
$file_ttl = idx($params, 'ttl');
|
|
|
|
$this->setTtl($file_ttl);
|
|
|
|
|
|
|
|
$view_policy = idx($params, 'viewPolicy');
|
|
|
|
if ($view_policy) {
|
|
|
|
$this->setViewPolicy($params['viewPolicy']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
|
|
|
|
$this->setIsExplicitUpload($is_explicit);
|
|
|
|
|
|
|
|
$can_cdn = idx($params, 'canCDN');
|
|
|
|
if ($can_cdn) {
|
|
|
|
$this->setCanCDN(true);
|
|
|
|
}
|
|
|
|
|
2015-03-01 21:12:38 +01:00
|
|
|
$builtin = idx($params, 'builtin');
|
|
|
|
if ($builtin) {
|
|
|
|
$this->setBuiltinName($builtin);
|
|
|
|
}
|
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
$mime_type = idx($params, 'mime-type');
|
|
|
|
if ($mime_type) {
|
|
|
|
$this->setMimeType($mime_type);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-08-20 00:53:15 +02:00
|
|
|
public function getRedirectResponse() {
|
|
|
|
$uri = $this->getBestURI();
|
|
|
|
|
|
|
|
// TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
|
|
|
|
// (if the file is a viewable image) and sometimes a local URI (if not).
|
|
|
|
// For now, just detect which one we got and configure the response
|
|
|
|
// appropriately. In the long run, if this endpoint is served from a CDN
|
|
|
|
// domain, we can't issue a local redirect to an info URI (which is not
|
|
|
|
// present on the CDN domain). We probably never actually issue local
|
|
|
|
// redirects here anyway, since we only ever transform viewable images
|
|
|
|
// right now.
|
|
|
|
|
|
|
|
$is_external = strlen(id(new PhutilURI($uri))->getDomain());
|
|
|
|
|
|
|
|
return id(new AphrontRedirectResponse())
|
|
|
|
->setIsExternal($is_external)
|
|
|
|
->setURI($uri);
|
|
|
|
}
|
|
|
|
|
2014-08-11 18:39:40 +02:00
|
|
|
|
2014-12-03 22:16:15 +01:00
|
|
|
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function getApplicationTransactionEditor() {
|
|
|
|
return new PhabricatorFileEditor();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getApplicationTransactionObject() {
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getApplicationTransactionTemplate() {
|
|
|
|
return new PhabricatorFileTransaction();
|
|
|
|
}
|
|
|
|
|
2014-12-04 22:58:52 +01:00
|
|
|
public function willRenderTimeline(
|
|
|
|
PhabricatorApplicationTransactionView $timeline,
|
|
|
|
AphrontRequest $request) {
|
|
|
|
|
|
|
|
return $timeline;
|
|
|
|
}
|
|
|
|
|
2014-12-03 22:16:15 +01:00
|
|
|
|
2012-10-31 17:57:46 +01:00
|
|
|
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function getCapabilities() {
|
|
|
|
return array(
|
|
|
|
PhabricatorPolicyCapability::CAN_VIEW,
|
2013-09-30 18:38:13 +02:00
|
|
|
PhabricatorPolicyCapability::CAN_EDIT,
|
2012-10-31 17:57:46 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPolicy($capability) {
|
2013-12-30 20:27:02 +01:00
|
|
|
switch ($capability) {
|
|
|
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
2015-03-01 21:12:38 +01:00
|
|
|
if ($this->isBuiltin()) {
|
|
|
|
return PhabricatorPolicies::getMostOpenPolicy();
|
|
|
|
}
|
2013-12-30 20:27:02 +01:00
|
|
|
return $this->getViewPolicy();
|
|
|
|
case PhabricatorPolicyCapability::CAN_EDIT:
|
|
|
|
return PhabricatorPolicies::POLICY_NOONE;
|
|
|
|
}
|
2012-10-31 17:57:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
2013-10-01 17:43:34 +02:00
|
|
|
$viewer_phid = $viewer->getPHID();
|
|
|
|
if ($viewer_phid) {
|
|
|
|
if ($this->getAuthorPHID() == $viewer_phid) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch ($capability) {
|
|
|
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
2014-09-04 21:49:31 +02:00
|
|
|
// If you can see the file this file is a transform of, you can see
|
|
|
|
// this file.
|
|
|
|
if ($this->getOriginalFile()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2013-10-01 17:43:34 +02:00
|
|
|
// If you can see any object this file is attached to, you can see
|
|
|
|
// the file.
|
|
|
|
return (count($this->getObjects()) > 0);
|
|
|
|
}
|
|
|
|
|
2012-10-31 17:57:46 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-09-27 17:43:41 +02:00
|
|
|
public function describeAutomaticCapability($capability) {
|
2013-10-01 17:43:34 +02:00
|
|
|
$out = array();
|
|
|
|
$out[] = pht('The user who uploaded a file can always view and edit it.');
|
|
|
|
switch ($capability) {
|
|
|
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
|
|
|
$out[] = pht(
|
|
|
|
'Files attached to objects are visible to users who can view '.
|
|
|
|
'those objects.');
|
2014-09-04 21:49:31 +02:00
|
|
|
$out[] = pht(
|
|
|
|
'Thumbnails are visible only to users who can view the original '.
|
|
|
|
'file.');
|
2013-10-01 17:43:34 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $out;
|
2013-09-27 17:43:41 +02:00
|
|
|
}
|
|
|
|
|
2013-09-05 22:11:02 +02:00
|
|
|
|
|
|
|
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function isAutomaticallySubscribed($phid) {
|
|
|
|
return ($this->authorPHID == $phid);
|
|
|
|
}
|
|
|
|
|
Make Projects a PhabricatorSubscribableInterface, but with restricted defaults
Summary:
Ref T4379. I want project subscriptions to work like this (yell if this seems whacky, since it makes subscriptions mean somethign a little different for projects than they do for other objects):
- You can only subscribe to a project if you're a project member.
- When you're added as a member, you're added as a subscriber.
- When you're removed as a member, you're removed as a subscriber.
- While you're a member, you can optionally unsubscribe.
From a UI perspective:
- We don't show the subscriber list, since it's going to be some uninteresting subset of the member list.
- We don't show CC transactions in history, since they're an uninteresting near-approximation of the membership transactions.
- You only see the subscription controls if you're a member.
To do this, I've augmented `PhabricatorSubscribableInterface` with two new methods. It would be nice if we were on PHP 5.4+ and could just use traits for this, but we should get data about version usage before we think about this. For now, copy/paste the default implementations into every implementing class.
Then, I implemented the interface in `PhabricatorProject` but with alternate defaults.
Test Plan:
- Used the normal interaction on existing objects.
- This has no actual effect on projects, verified no subscription stuff mysteriously appeared.
- Hit the new error case by fiddling with the UI.
Reviewers: btrahan
Reviewed By: btrahan
CC: chad, aran
Maniphest Tasks: T4379
Differential Revision: https://secure.phabricator.com/D8165
2014-02-10 23:29:17 +01:00
|
|
|
public function shouldShowSubscribersProperty() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function shouldAllowSubscription($phid) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2013-09-05 22:11:02 +02:00
|
|
|
|
|
|
|
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function getUsersToNotifyOfTokenGiven() {
|
|
|
|
return array(
|
|
|
|
$this->getAuthorPHID(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-08-20 00:44:27 +02:00
|
|
|
/* -( PhabricatorDestructibleInterface )----------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
public function destroyObjectPermanently(
|
|
|
|
PhabricatorDestructionEngine $engine) {
|
|
|
|
|
|
|
|
$this->openTransaction();
|
|
|
|
$this->delete();
|
|
|
|
$this->saveTransaction();
|
|
|
|
}
|
|
|
|
|
2011-01-23 03:33:00 +01:00
|
|
|
}
|