diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 09e6f6c6de..a94dc510f5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -17,8 +17,11 @@ phutil_register_library_map(array( 'AphrontDialogResponse' => 'aphront/response/dialog', 'AphrontDialogView' => 'view/dialog', 'AphrontErrorView' => 'view/form/error', + 'AphrontFileResponse' => 'aphront/response/file', 'AphrontFormControl' => 'view/form/control/base', + 'AphrontFormFileControl' => 'view/form/control/file', 'AphrontFormSelectControl' => 'view/form/control/select', + 'AphrontFormStaticControl' => 'view/form/control/static', 'AphrontFormSubmitControl' => 'view/form/control/submit', 'AphrontFormTextAreaControl' => 'view/form/control/textarea', 'AphrontFormTextControl' => 'view/form/control/text', @@ -59,6 +62,13 @@ phutil_register_library_map(array( 'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit', 'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist', 'PhabricatorDirectoryMainController' => 'applications/directory/controller/main', + 'PhabricatorFile' => 'applications/files/storage/file', + 'PhabricatorFileController' => 'applications/files/controller/base', + 'PhabricatorFileDAO' => 'applications/files/storage/base', + 'PhabricatorFileListController' => 'applications/files/controller/list', + 'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob', + 'PhabricatorFileUploadController' => 'applications/files/controller/upload', + 'PhabricatorFileViewController' => 'applications/files/controller/view', 'PhabricatorLiskDAO' => 'applications/base/storage/lisk', 'PhabricatorPHID' => 'applications/phid/storage/phid', 'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate', @@ -90,8 +100,11 @@ phutil_register_library_map(array( 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => 'AphrontView', 'AphrontErrorView' => 'AphrontView', + 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormControl' => 'AphrontView', + 'AphrontFormFileControl' => 'AphrontFormControl', 'AphrontFormSelectControl' => 'AphrontFormControl', + 'AphrontFormStaticControl' => 'AphrontFormControl', 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', @@ -121,6 +134,13 @@ phutil_register_library_map(array( 'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController', + 'PhabricatorFile' => 'PhabricatorFileDAO', + 'PhabricatorFileController' => 'PhabricatorController', + 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', + 'PhabricatorFileListController' => 'PhabricatorFileController', + 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', + 'PhabricatorFileUploadController' => 'PhabricatorFileController', + 'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorPHID' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 4765cf56e6..0939448591 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -51,6 +51,13 @@ class AphrontDefaultApplicationConfiguration 'category/delete/(?\d+)/' => 'PhabricatorDirectoryCategoryDeleteController', ), + '/file/' => array( + '$' => 'PhabricatorFileListController', + 'upload/$' => 'PhabricatorFileUploadController', + '(?info)/(?[^/]+)/' => 'PhabricatorFileViewController', + '(?view)/(?[^/]+)/' => 'PhabricatorFileViewController', + '(?download)/(?[^/]+)/' => 'PhabricatorFileViewController', + ), '/phid/' => array( '$' => 'PhabricatorPHIDListController', 'type/$' => 'PhabricatorPHIDTypeListController', diff --git a/src/aphront/response/file/AphrontFileResponse.php b/src/aphront/response/file/AphrontFileResponse.php new file mode 100644 index 0000000000..0e60435951 --- /dev/null +++ b/src/aphront/response/file/AphrontFileResponse.php @@ -0,0 +1,70 @@ +download = $download; + return $this; + } + + public function getDownload() { + return $this->download; + } + + public function setMimeType($mime_type) { + $this->mimeType = $mime_type; + return $this; + } + + public function getMimeType() { + return $this->mimeType; + } + + public function setContent($content) { + $this->content = $content; + return $this; + } + + public function buildResponseString() { + return $this->content; + } + + public function getHeaders() { + $headers = array( + array('Content-Type', $this->getMimeType()), + ); + + if ($this->getDownload()) { + $headers[] = array( + 'Content-Disposition', + 'attachment; filename='.$this->getDownload(), + ); + } + + return $headers; + } + +} diff --git a/src/applications/files/controller/base/PhabricatorFileController.php b/src/applications/files/controller/base/PhabricatorFileController.php new file mode 100644 index 0000000000..6ef76df91b --- /dev/null +++ b/src/applications/files/controller/base/PhabricatorFileController.php @@ -0,0 +1,34 @@ +setApplicationName('Files'); + $page->setBaseURI('/file/'); + $page->setTitle(idx($data, 'title')); + $page->setGlyph("\xE2\x87\xAA"); + $page->appendChild($view); + + $response = new AphrontWebpageResponse(); + return $response->setContent($page->render()); + } + +} diff --git a/src/applications/files/controller/base/__init__.php b/src/applications/files/controller/base/__init__.php new file mode 100644 index 0000000000..16685a0b26 --- /dev/null +++ b/src/applications/files/controller/base/__init__.php @@ -0,0 +1,16 @@ +loadAllWhere( + '1 = 1 ORDER BY id DESC LIMIT 100'); + + $rows = array(); + foreach ($files as $file) { + $rows[] = array( + phutil_escape_html($file->getPHID()), + phutil_escape_html($file->getName()), + phutil_escape_html($file->getByteSize()), + phutil_render_tag( + 'a', + array( + 'class' => 'small button grey', + 'href' => '/file/info/'.$file->getPHID().'/', + ), + 'Info'), + phutil_render_tag( + 'a', + array( + 'class' => 'small button grey', + 'href' => '/file/view/'.$file->getPHID().'/', + ), + 'View'), + phutil_render_tag( + 'a', + array( + 'class' => 'small button grey', + 'href' => '/file/download/'.$file->getPHID().'/', + ), + 'Download'), + ); + } + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + 'PHID', + 'Name', + 'Size', + '', + '', + '', + )); + $table->setColumnClasses( + array( + null, + 'wide', + null, + 'action', + 'action', + 'action', + )); + + $panel = new AphrontPanelView(); + $panel->appendChild($table); + $panel->setHeader('Files'); + $panel->setCreateButton('Upload File', '/file/upload/'); + + return $this->buildStandardPageResponse($panel, array( + 'title' => 'Files', + 'tab' => 'files', + )); + } +} diff --git a/src/applications/files/controller/list/__init__.php b/src/applications/files/controller/list/__init__.php new file mode 100644 index 0000000000..91678ceb0e --- /dev/null +++ b/src/applications/files/controller/list/__init__.php @@ -0,0 +1,18 @@ +getRequest(); + if ($request->isFormPost()) { + $file = PhabricatorFile::newFromPHPUpload( + idx($_FILES, 'file'), + array( + 'name' => $request->getStr('name'), + )); + + return id(new AphrontRedirectResponse()) + ->setURI('/file/info/'.phutil_escape_uri($file->getPHID()).'/'); + } + + $form = new AphrontFormView(); + $form->setAction('/file/upload/'); + + $form + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setLabel('File') + ->setName('file') + ->setError(true)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setCaption('Optional file display name.')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Upload') + ->addCancelButton('/file/')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Upload File'); + + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + + return $this->buildStandardPageResponse( + array($panel), + array( + 'title' => 'Upload File', + )); + } + +} diff --git a/src/applications/files/controller/upload/__init__.php b/src/applications/files/controller/upload/__init__.php new file mode 100644 index 0000000000..63a11bf54e --- /dev/null +++ b/src/applications/files/controller/upload/__init__.php @@ -0,0 +1,20 @@ +phid = $data['phid']; + $this->view = $data['view']; + } + + public function processRequest() { + + $file = id(new PhabricatorFile())->loadOneWhere( + 'phid = %s', + $this->phid); + if (!$file) { + return new Aphront404Response(); + } + + switch ($this->view) { + case 'download': + case 'view': + $data = $file->loadFileData(); + $response = new AphrontFileResponse(); + $response->setContent($data); + $response->setMimeType($file->getMimeType()); + if ($this->view == 'download') { + $response->setDownload($file->getName()); + } + return $response; + default: + break; + } + + $form = new AphrontFormView(); + $form->setAction('/file/view/'.$file->getPHID().'/'); + $form + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($file->getName())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('PHID') + ->setName('phid') + ->setValue($file->getPHID())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Created') + ->setName('created') + ->setValue(date('Y-m-d g:i:s A', $file->getDateCreated()))) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Mime Type') + ->setName('mime') + ->setValue($file->getMimeType())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Size') + ->setName('size') + ->setValue($file->getByteSize().' bytes')) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Engine') + ->setName('storageEngine') + ->setValue($file->getStorageEngine())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Format') + ->setName('storageFormat') + ->setValue($file->getStorageFormat())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('Handle') + ->setName('storageHandle') + ->setValue($file->getStorageHandle())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('View File')); + + $panel = new AphrontPanelView(); + $panel->setHeader('File Info - '.$file->getName()); + + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + + return $this->buildStandardPageResponse( + array($panel), + array( + 'title' => 'File Info - '.$file->getName(), + )); + } +} diff --git a/src/applications/files/controller/view/__init__.php b/src/applications/files/controller/view/__init__.php new file mode 100644 index 0000000000..ed0b9e9534 --- /dev/null +++ b/src/applications/files/controller/view/__init__.php @@ -0,0 +1,20 @@ + true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID(self::PHID_TYPE); + } + + 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()); + + 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. + } + + $file->save(); + + return $file; + } + + 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; + } + +} diff --git a/src/applications/files/storage/file/__init__.php b/src/applications/files/storage/file/__init__.php new file mode 100644 index 0000000000..26394989c8 --- /dev/null +++ b/src/applications/files/storage/file/__init__.php @@ -0,0 +1,19 @@ +setPHIDType($type); diff --git a/src/view/form/base/AphrontFormView.php b/src/view/form/base/AphrontFormView.php index bb03038d86..5cf93d98f4 100755 --- a/src/view/form/base/AphrontFormView.php +++ b/src/view/form/base/AphrontFormView.php @@ -22,6 +22,7 @@ final class AphrontFormView extends AphrontView { private $method = 'POST'; private $header; private $data = array(); + private $encType; public function setAction($action) { $this->action = $action; @@ -33,13 +34,19 @@ final class AphrontFormView extends AphrontView { return $this; } + public function setEncType($enc_type) { + $this->encType = $enc_type; + return $this; + } + public function render() { return phutil_render_tag( 'form', array( - 'action' => $this->action, - 'method' => $this->method, - 'class' => 'aphront-form-view', + 'action' => $this->action, + 'method' => $this->method, + 'class' => 'aphront-form-view', + 'enctype' => $this->encType, ), $this->renderDataInputs(). $this->renderChildren()); diff --git a/src/view/form/control/file/AphrontFormFileControl.php b/src/view/form/control/file/AphrontFormFileControl.php new file mode 100755 index 0000000000..9c83ceaaf9 --- /dev/null +++ b/src/view/form/control/file/AphrontFormFileControl.php @@ -0,0 +1,35 @@ + 'file', + 'name' => $this->getName(), + 'disabled' => $this->getDisabled() ? 'disabled' : null, + )); + } + +} diff --git a/src/view/form/control/file/__init__.php b/src/view/form/control/file/__init__.php new file mode 100644 index 0000000000..ccabdcbaa2 --- /dev/null +++ b/src/view/form/control/file/__init__.php @@ -0,0 +1,14 @@ +getValue()); + } + +} diff --git a/src/view/form/control/static/__init__.php b/src/view/form/control/static/__init__.php new file mode 100644 index 0000000000..97d6bb9b42 --- /dev/null +++ b/src/view/form/control/static/__init__.php @@ -0,0 +1,14 @@ + '__submit__', - 'disabled' => $this->getDisabled() ? 'disabled' : null, - ), - phutil_escape_html($this->getValue())). - $this->cancelButton; + $submit_button = null; + if ($this->getValue()) { + $submit_button = phutil_render_tag( + 'button', + array( + 'name' => '__submit__', + 'disabled' => $this->getDisabled() ? 'disabled' : null, + ), + phutil_escape_html($this->getValue())); + } + return $submit_button.$this->cancelButton; } } diff --git a/webroot/rsrc/css/base.css b/webroot/rsrc/css/base.css index 4e49a892d3..f305a544c2 100644 --- a/webroot/rsrc/css/base.css +++ b/webroot/rsrc/css/base.css @@ -476,8 +476,6 @@ a.small:visited { clear: both; margin-right: 25%; margin-left: 15%; - margin-top: 3px; - margin-bottom: 6px; } .aphront-error-view {