2011-01-22 18:33:00 -08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Copyright 2011 Facebook, Inc.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
class PhabricatorFile extends PhabricatorFileDAO {
|
|
|
|
|
|
|
|
|
|
const STORAGE_ENGINE_BLOB = 'blob';
|
|
|
|
|
|
|
|
|
|
const STORAGE_FORMAT_RAW = 'raw';
|
|
|
|
|
|
|
|
|
|
// TODO: We need to reconcile this with MySQL packet size.
|
|
|
|
|
const FILE_SIZE_BYTE_LIMIT = 12582912;
|
|
|
|
|
|
|
|
|
|
protected $phid;
|
|
|
|
|
protected $name;
|
|
|
|
|
protected $mimeType;
|
|
|
|
|
protected $byteSize;
|
|
|
|
|
|
|
|
|
|
protected $storageEngine;
|
|
|
|
|
protected $storageFormat;
|
|
|
|
|
protected $storageHandle;
|
|
|
|
|
|
|
|
|
|
public function getConfiguration() {
|
|
|
|
|
return array(
|
|
|
|
|
self::CONFIG_AUX_PHID => true,
|
|
|
|
|
) + parent::getConfiguration();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function generatePHID() {
|
2011-03-02 18:58:21 -08:00
|
|
|
return PhabricatorPHID::generateNewPHID(
|
|
|
|
|
PhabricatorPHIDConstants::PHID_TYPE_FILE);
|
2011-01-22 18:33:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function newFromPHPUpload($spec, array $params = array()) {
|
|
|
|
|
if (!$spec) {
|
|
|
|
|
throw new Exception("No file was uploaded!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$err = idx($spec, 'error');
|
|
|
|
|
if ($err) {
|
|
|
|
|
throw new Exception("File upload failed with error '{$err}'.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$tmp_name = idx($spec, 'tmp_name');
|
|
|
|
|
$is_valid = @is_uploaded_file($tmp_name);
|
|
|
|
|
if (!$is_valid) {
|
|
|
|
|
throw new Exception("File is not an uploaded file.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$file_data = Filesystem::readFile($tmp_name);
|
|
|
|
|
$file_size = idx($spec, 'size');
|
|
|
|
|
|
|
|
|
|
if (strlen($file_data) != $file_size) {
|
|
|
|
|
throw new Exception("File size disagrees with uploaded size.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$file_name = nonempty(
|
|
|
|
|
idx($params, 'name'),
|
|
|
|
|
idx($spec, 'name'));
|
|
|
|
|
$params = array(
|
|
|
|
|
'name' => $file_name,
|
|
|
|
|
) + $params;
|
|
|
|
|
|
|
|
|
|
return self::newFromFileData($file_data, $params);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function newFromFileData($data, array $params = array()) {
|
|
|
|
|
$file_size = strlen($data);
|
|
|
|
|
|
|
|
|
|
if ($file_size > self::FILE_SIZE_BYTE_LIMIT) {
|
|
|
|
|
throw new Exception("File is too large to store.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$file_name = idx($params, 'name');
|
|
|
|
|
$file_name = self::normalizeFileName($file_name);
|
|
|
|
|
|
|
|
|
|
$file = new PhabricatorFile();
|
|
|
|
|
$file->setName($file_name);
|
|
|
|
|
$file->setByteSize(strlen($data));
|
|
|
|
|
|
|
|
|
|
$blob = new PhabricatorFileStorageBlob();
|
|
|
|
|
$blob->setData($data);
|
|
|
|
|
$blob->save();
|
|
|
|
|
|
|
|
|
|
// TODO: This stuff is almost certainly YAGNI, but we could imagine having
|
|
|
|
|
// an alternate disk store and gzipping or encrypting things or something
|
|
|
|
|
// crazy like that and this isn't toooo much extra code.
|
|
|
|
|
$file->setStorageEngine(self::STORAGE_ENGINE_BLOB);
|
|
|
|
|
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
|
|
|
|
|
$file->setStorageHandle($blob->getID());
|
|
|
|
|
|
2011-02-02 13:48:52 -08:00
|
|
|
if (isset($params['mime-type'])) {
|
|
|
|
|
$file->setMimeType($params['mime-type']);
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
$tmp = new TempFile();
|
|
|
|
|
Filesystem::writeFile($tmp, $data);
|
|
|
|
|
list($stdout) = execx('file -b --mime %s', $tmp);
|
|
|
|
|
$file->setMimeType($stdout);
|
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
|
// Be robust here since we don't really care that much about mime types.
|
|
|
|
|
}
|
2011-01-22 18:33:00 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$file->save();
|
|
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
|
}
|
|
|
|
|
|
2011-04-13 15:15:48 -07:00
|
|
|
public static function newFromFileDownload($uri, $name) {
|
|
|
|
|
$uri = new PhutilURI($uri);
|
2011-05-02 14:20:24 -07:00
|
|
|
|
|
|
|
|
$protocol = $uri->getProtocol();
|
|
|
|
|
switch ($protocol) {
|
|
|
|
|
case 'http':
|
|
|
|
|
case 'https':
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// Make sure we are not accessing any file:// URIs or similar.
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2011-04-13 15:15:48 -07:00
|
|
|
$timeout = stream_context_create(
|
|
|
|
|
array(
|
|
|
|
|
'http' => array(
|
|
|
|
|
'timeout' => 5,
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
$file_data = @file_get_contents($uri, false, $timeout);
|
|
|
|
|
if ($file_data === false) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::newFromFileData($file_data, array('name' => $name));
|
|
|
|
|
}
|
|
|
|
|
|
2011-01-22 18:33:00 -08:00
|
|
|
public static function normalizeFileName($file_name) {
|
|
|
|
|
return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function delete() {
|
|
|
|
|
$this->openTransaction();
|
|
|
|
|
switch ($this->getStorageEngine()) {
|
|
|
|
|
case self::STORAGE_ENGINE_BLOB:
|
|
|
|
|
$handle = $this->getStorageHandle();
|
|
|
|
|
$blob = id(new PhabricatorFileStorageBlob())->load($handle);
|
|
|
|
|
$blob->delete();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception("Unknown storage engine!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ret = parent::delete();
|
|
|
|
|
$this->saveTransaction();
|
|
|
|
|
return $ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function loadFileData() {
|
|
|
|
|
|
|
|
|
|
$handle = $this->getStorageHandle();
|
|
|
|
|
$data = null;
|
|
|
|
|
|
|
|
|
|
switch ($this->getStorageEngine()) {
|
|
|
|
|
case self::STORAGE_ENGINE_BLOB:
|
|
|
|
|
$blob = id(new PhabricatorFileStorageBlob())->load($handle);
|
|
|
|
|
if (!$blob) {
|
|
|
|
|
throw new Exception("Failed to load file blob data.");
|
|
|
|
|
}
|
|
|
|
|
$data = $blob->getData();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception("Unknown storage engine.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch ($this->getStorageFormat()) {
|
|
|
|
|
case self::STORAGE_FORMAT_RAW:
|
|
|
|
|
$data = $data;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception("Unknown storage format.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $data;
|
|
|
|
|
}
|
|
|
|
|
|
2011-01-26 09:02:09 -08:00
|
|
|
public function getViewURI() {
|
|
|
|
|
return PhabricatorFileURI::getViewURIForPHID($this->getPHID());
|
|
|
|
|
}
|
2011-02-22 09:22:57 -08:00
|
|
|
|
Improve drag-and-drop uploader
Summary:
Make it discoverable, show uploading progress, show file thumbnails, allow you
to remove files, make it a generic form component.
Test Plan:
Uploaded ducks
Reviewed By: tomo
Reviewers: aran, tomo, jungejason, tuomaspelkonen
CC: anjali, aran, epriestley, tomo
Differential Revision: 334
2011-05-22 16:11:41 -07:00
|
|
|
public function getThumb60x45URI() {
|
|
|
|
|
return '/file/xform/thumb-60x45/'.$this->getPHID().'/';
|
|
|
|
|
}
|
|
|
|
|
|
2011-05-22 17:06:42 -07:00
|
|
|
public function getThumb160x120URI() {
|
|
|
|
|
return '/file/xform/thumb-160x120/'.$this->getPHID().'/';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2011-02-22 09:19:14 -08:00
|
|
|
public function isViewableInBrowser() {
|
|
|
|
|
return ($this->getViewableMimeType() !== null);
|
|
|
|
|
}
|
2011-02-22 09:22:57 -08:00
|
|
|
|
2011-05-22 14:40:51 -07: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 08:43:42 -07: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 14:40:51 -07:00
|
|
|
}
|
|
|
|
|
|
2011-02-22 09:19:14 -08:00
|
|
|
public function getViewableMimeType() {
|
|
|
|
|
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
|
|
|
|
|
|
|
|
|
|
$mime_type = $this->getMimeType();
|
|
|
|
|
$mime_parts = explode(';', $mime_type);
|
2011-02-28 10:15:42 -08:00
|
|
|
$mime_type = trim(reset($mime_parts));
|
2011-02-22 09:22:57 -08:00
|
|
|
|
2011-02-22 09:19:14 -08:00
|
|
|
return idx($mime_map, $mime_type);
|
|
|
|
|
}
|
2011-01-26 09:02:09 -08:00
|
|
|
|
2011-01-22 18:33:00 -08:00
|
|
|
}
|