diff --git a/src/applications/differential/controller/diffcreate/DifferentialDiffCreateController.php b/src/applications/differential/controller/diffcreate/DifferentialDiffCreateController.php index 86416188bc..c21ded7164 100644 --- a/src/applications/differential/controller/diffcreate/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/diffcreate/DifferentialDiffCreateController.php @@ -1,73 +1,84 @@ <?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 DifferentialDiffCreateController extends DifferentialController { public function processRequest() { $request = $this->getRequest(); if ($request->isFormPost()) { $parser = new ArcanistDiffParser(); - $diff = $request->getStr('diff'); + $diff = null; + try { + $diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']); + } catch (Exception $ex) { + $diff = $request->getStr('diff'); + } $changes = $parser->parseDiff($diff); $diff = DifferentialDiff::newFromRawChanges($changes); $diff->setLintStatus(DifferentialLintStatus::LINT_SKIP); $diff->setUnitStatus(DifferentialLintStatus::LINT_SKIP); $diff->setAuthorPHID($request->getUser()->getPHID()); $diff->setCreationMethod('web'); $diff->save(); return id(new AphrontRedirectResponse()) ->setURI('/differential/diff/'.$diff->getID().'/'); } $form = new AphrontFormView(); $form ->setAction('/differential/diff/create/') + ->setEncType('multipart/form-data') ->setUser($request->getUser()) ->appendChild( '<p class="aphront-form-instructions">The best way to create a '. 'Differential diff is by using <strong>Arcanist</strong>, but you '. 'can also just paste a diff (e.g., from <tt>svn diff</tt> or '. - '<tt>git diff</tt>) into this box if you really want.</p>') + '<tt>git diff</tt>) into this box or upload it as a file if you '. + 'really want.</p>') ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Raw Diff') ->setName('diff') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)) + ->appendChild( + id(new AphrontFormFileControl()) + ->setLabel('Raw Diff from file') + ->setName('diff-file')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue("Create Diff \xC2\xBB")); $panel = new AphrontPanelView(); $panel->setHeader('Create New Diff'); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Create Diff', 'tab' => 'create', )); } } diff --git a/src/applications/differential/controller/diffcreate/__init__.php b/src/applications/differential/controller/diffcreate/__init__.php index fd56eba3fb..3bc3857036 100644 --- a/src/applications/differential/controller/diffcreate/__init__.php +++ b/src/applications/differential/controller/diffcreate/__init__.php @@ -1,23 +1,25 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('arcanist', 'parser/diff'); phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/differential/constants/lintstatus'); phutil_require_module('phabricator', 'applications/differential/controller/base'); phutil_require_module('phabricator', 'applications/differential/storage/diff'); +phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'view/form/base'); +phutil_require_module('phabricator', 'view/form/control/file'); phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/control/textarea'); phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phutil', 'utils'); phutil_require_source('DifferentialDiffCreateController.php'); diff --git a/src/applications/files/storage/file/PhabricatorFile.php b/src/applications/files/storage/file/PhabricatorFile.php index f380a89660..9f8199de18 100644 --- a/src/applications/files/storage/file/PhabricatorFile.php +++ b/src/applications/files/storage/file/PhabricatorFile.php @@ -1,324 +1,330 @@ <?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_FORMAT_RAW = 'raw'; protected $phid; protected $name; protected $mimeType; protected $byteSize; protected $authorPHID; protected $storageEngine; protected $storageFormat; protected $storageHandle; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_FILE); } - public static function newFromPHPUpload($spec, array $params = array()) { + public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception("No file was uploaded!"); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($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."); } + return $file_data; + } + + public static function newFromPHPUpload($spec, array $params = array()) { + $file_data = self::readUploadedFileData($spec); + $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()) { $selector_class = PhabricatorEnv::getEnvConfig('storage.engine-selector'); $selector = newv($selector_class, array()); $engines = $selector->selectStorageEngines($data, $params); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $data_handle = null; $engine_identifier = null; foreach ($engines as $engine) { try { // Perform the actual write. $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new Exception( "Storage engine '{$engine}' 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 Exception( "Storage engine '{$engine}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } // We stored the file somewhere so stop trying to write it to other // places. break; } catch (Exception $ex) { // If an engine doesn't work, keep trying all the other valid engines // in case something else works. phlog($ex); } } if (!$data_handle) { throw new Exception("All storage engines failed to write file!"); } $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); // If for whatever reason, authorPHID isn't passed as a param // (always the case with newFromFileDownload()), store a '' $authorPHID = idx($params, 'authorPHID'); $file = new PhabricatorFile(); $file->setName($file_name); $file->setByteSize(strlen($data)); $file->setAuthorPHID($authorPHID); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); // TODO: This is probably YAGNI, but allows for us to do encryption or // compression later if we want. $file->setStorageFormat(self::STORAGE_FORMAT_RAW); 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. } } $file->save(); return $file; } public static function newFromFileDownload($uri, $name) { $uri = new PhutilURI($uri); $protocol = $uri->getProtocol(); switch ($protocol) { case 'http': case 'https': break; default: // Make sure we are not accessing any file:// URIs or similar. return null; } $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)); } public static function normalizeFileName($file_name) { return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name); } public function delete() { $engine = $this->instantiateStorageEngine(); $ret = parent::delete(); $engine->deleteFile($this->getStorageHandle()); return $ret; } public function loadFileData() { $engine = $this->instantiateStorageEngine(); $data = $engine->readFile($this->getStorageHandle()); switch ($this->getStorageFormat()) { case self::STORAGE_FORMAT_RAW: $data = $data; break; default: throw new Exception("Unknown storage format."); } return $data; } public function getViewURI() { $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); if ($alt) { $path = '/file/alt/'.$this->generateSecretKey().'/'.$this->getPHID().'/'; $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } else { return '/file/view/'.$this->getPHID().'/'; } } public function getInfoURI() { return '/file/info/'.$this->getPHID().'/'; } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getThumb60x45URI() { return '/file/xform/thumb-60x45/'.$this->getPHID().'/'; } public function getThumb160x120URI() { return '/file/xform/thumb-160x120/'.$this->getPHID().'/'; } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isTransformableImage() { // 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.'); } } protected function instantiateStorageEngine() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); foreach ($engines as $engine_class) { $engine = newv($engine_class['name'], array()); if ($engine->getEngineIdentifier() == $this->getStorageEngine()) { return $engine; } } throw new Exception("File's storage engine could be located!"); } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } public function validateSecretKey($key) { return ($key == $this->generateSecretKey()); } private function generateSecretKey() { $file_key = PhabricatorEnv::getEnvConfig('phabricator.file-key'); $hash = sha1($this->phid.$this->storageHandle.$file_key); return substr($hash, 0, 20); } }