diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 0f4b6927b9..f57e5bdb32 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -1,280 +1,287 @@ conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $conpherence_id = $this->getConpherenceID(); if (!$conpherence_id) { return new Aphront404Response(); } $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needOrigPics(true) ->needHeaderPics(true) ->executeOne(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $action = $request->getStr('action', 'metadata'); $latest_transaction_id = null; $fancy_ajax_style = true; $error_view = null; $e_file = array(); $errors = array(); if ($request->isFormPost()) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr() )); $editor = id(new ConpherenceEditor()) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSource($content_source) ->setActor($user); switch ($action) { case 'message': $message = $request->getStr('text'); $latest_transaction_id = $request->getInt('latest_transaction_id'); $xactions = $editor->generateTransactionsFromText( $conpherence, $message); break; case 'metadata': $xactions = array(); $top = $request->getInt('image_y'); $left = $request->getInt('image_x'); $file_id = $request->getInt('file_id'); $title = $request->getStr('title'); $updated = false; if ($file_id) { $orig_file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withIDs(array($file_id)) ->executeOne(); $okay = $orig_file->isTransformableImage(); if ($okay) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE) ->setNewValue($orig_file->getPHID()); - // do 2 transformations "crudely" + // do a transformation "crudely" $xformer = new PhabricatorImageTransformer(); $header_file = $xformer->executeConpherenceTransform( $orig_file, 0, 0, ConpherenceImageData::HEAD_WIDTH, ConpherenceImageData::HEAD_HEIGHT); // this is handled outside the editor for now. no particularly // good reason to move it inside $conpherence->setImagePHIDs( array( ConpherenceImageData::SIZE_HEAD => $header_file->getPHID(), )); $conpherence->setImages( array( ConpherenceImageData::SIZE_HEAD => $header_file, )); } else { $e_file[] = $orig_file; $errors[] = pht('This server only supports these image formats: %s.', implode(', ', $supported_formats)); } // use the existing title in this image upload case $title = $conpherence->getTitle(); $updated = true; $fancy_ajax_style = false; - } else if ($top !== null || $left !== null) { + } + + // all other metadata updates are continue requests + if (!$request->isContinueRequest()) { + break; + } + + if ($top !== null || $left !== null) { $file = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeConpherenceTransform( $file, $top, $left, ConpherenceImageData::HEAD_WIDTH, ConpherenceImageData::HEAD_HEIGHT); $image_phid = $xformed->getPHID(); $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PICTURE_CROP) ->setNewValue($image_phid); $updated = true; } if ($title != $conpherence->getTitle()) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE) ->setNewValue($title); $updated = true; } - if (!$updated && $request->isContinueRequest()) { + if (!$updated) { $errors[] = pht( 'That was a non-update. Try cancel.'); } break; default: throw new Exception('Unknown action: '.$action); break; } if ($xactions) { try { $xactions = $editor->applyTransactions($conpherence, $xactions); if ($fancy_ajax_style) { $content = $this->loadAndRenderUpdates( $conpherence_id, $latest_transaction_id); return id(new AphrontAjaxResponse()) ->setContent($content); } else { return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI($conpherence->getID().'/')); } } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($this->getApplicationURI($conpherence_id.'/')) ->setException($ex); } } } if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle(pht('Errors editing conpherence.')) ->setInsideDialogue(true) ->setErrors($errors); } switch ($action) { case 'metadata': default: $dialogue = $this->renderMetadataDialogue($conpherence, $error_view); break; } return id(new AphrontDialogResponse()) ->setDialog($dialogue ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/')) ->addSubmitButton() ->addCancelButton($this->getApplicationURI($conpherence->getID().'/'))); } private function renderMetadataDialogue( ConpherenceThread $conpherence, $error_view) { $form = id(new AphrontFormLayoutView()) ->appendChild($error_view) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setName('title') ->setValue($conpherence->getTitle())); $image = $conpherence->getImage(ConpherenceImageData::SIZE_ORIG); if ($image) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Image')) ->setValue(phutil_tag( 'img', array( 'src' => $conpherence->loadImageURI(ConpherenceImageData::SIZE_HEAD), )))) ->appendChild( id(new AphrontFormCropControl()) ->setLabel(pht('Crop Image')) ->setValue($image) ->setWidth(ConpherenceImageData::HEAD_WIDTH) ->setHeight(ConpherenceImageData::HEAD_HEIGHT)) ->appendChild( id(new ConpherenceFormDragAndDropUploadControl()) ->setLabel(pht('Change Image'))); } else { $form ->appendChild( id(new ConpherenceFormDragAndDropUploadControl()) ->setLabel(pht('Image'))); } require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogView()) ->setTitle(pht('Update Conpherence')) ->addHiddenInput('action', 'metadata') ->addHiddenInput('__continue__', true) ->appendChild($form); } private function loadAndRenderUpdates( $conpherence_id, $latest_transaction_id) { $user = $this->getRequest()->getUser(); $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->setAfterID($latest_transaction_id) ->needHeaderPics(true) ->needWidgetData(true) ->withIDs(array($conpherence_id)) ->executeOne(); $data = $this->renderConpherenceTransactions($conpherence); $rendered_transactions = $data['transactions']; $new_latest_transaction_id = $data['latest_transaction_id']; $selected = true; $nav_item = $this->buildConpherenceMenuItem( $conpherence, '-nav-item', $selected); $menu_item = $this->buildConpherenceMenuItem( $conpherence, '-menu-item', $selected); $header = $this->buildHeaderPaneContent($conpherence); $file_widget = id(new ConpherenceFileWidgetView()) ->setUser($this->getRequest()->getUser()) ->setConpherence($conpherence) ->setUpdateURI( $this->getApplicationURI('update/'.$conpherence->getID().'/')); $content = array( 'transactions' => $rendered_transactions, 'latest_transaction_id' => $new_latest_transaction_id, 'menu_item' => $menu_item->render(), 'nav_item' => $nav_item->render(), 'conpherence_phid' => $conpherence->getPHID(), 'header' => $header, 'file_widget' => $file_widget->render() ); return $content; } } diff --git a/src/applications/conpherence/view/ConpherenceFileWidgetView.php b/src/applications/conpherence/view/ConpherenceFileWidgetView.php index 97a6b5d829..6d790ea5e2 100644 --- a/src/applications/conpherence/view/ConpherenceFileWidgetView.php +++ b/src/applications/conpherence/view/ConpherenceFileWidgetView.php @@ -1,111 +1,111 @@ updateURI = $update_uri; return $this; } public function getUpdateURI() { return $this->updateURI; } public function setConpherence(ConpherenceThread $conpherence) { $this->conpherence = $conpherence; return $this; } public function getConpherence() { return $this->conpherence; } public function render() { require_celerity_resource('sprite-docs-css'); $conpherence = $this->getConpherence(); $widget_data = $conpherence->getWidgetData(); $files = $widget_data['files']; $files_authors = $widget_data['files_authors']; $files_html = array(); foreach ($files as $file) { $icon_class = $file->getDisplayIconForMimeType(); $icon_view = phutil_tag( 'div', array( 'class' => 'file-icon sprite-docs '.$icon_class ), ''); $file_view = id(new PhabricatorFileLinkView()) ->setFilePHID($file->getPHID()) - ->setFileName($file->getName()) + ->setFileName(phutil_utf8_shorten($file->getName(), 38)) ->setFileViewable($file->isViewableImage()) ->setFileViewURI($file->getBestURI()) ->setCustomClass('file-title'); $who_done_it_text = ''; // system generated files don't have authors if ($file->getAuthorPHID()) { $who_done_it_text = pht( 'by %s ', $files_authors[$file->getPHID()]->renderLink()); } $date_text = phabricator_relative_date( $file->getDateCreated(), $this->getUser()); $who_done_it = phutil_tag( 'div', array( 'class' => 'file-uploaded-by' ), pht('Uploaded %s%s.', $who_done_it_text, $date_text)); $extra = ''; if ($file->isViewableImage()) { $meta = $file_view->getMetadata(); $extra = javelin_tag( 'a', array( 'sigil' => 'lightboxable', 'meta' => $meta, 'class' => 'file-extra', ), phutil_tag( 'img', array( 'src' => $file->getThumb160x120URI() ), '')); } $divider = phutil_tag( 'div', array( 'class' => 'divider' ), ''); $files_html[] = phutil_tag( 'div', array( 'class' => 'file-entry' ), array( $icon_view, $file_view, $who_done_it, $extra, $divider )); } return phutil_tag( 'div', array('class' => 'file-list'), $files_html); } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index e746fd1a53..dcbb1d6153 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,674 +1,689 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_FILE); } 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."); } self::validateFileSize(strlen($file_data)); 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 newFromXHRUpload($data, array $params = array()) { self::validateFileSize(strlen($data)); return self::newFromFileData($data, $params); } private static function validateFileSize($size) { $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); if (!$limit) { return; } $limit = phabricator_parse_bytes($limit); if ($size > $limit) { throw new PhabricatorFileUploadException(-1000); } } /** * 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( 'name = %s AND contentHash = %s LIMIT 1', self::normalizeFileName(idx($params, 'name')), self::hashFileContent($data)); if (!$file) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = PhabricatorFile::newFromFileData($data, $params); unset($unguarded); } return $file; } public static function newFileFromContentHash($hash, $params) { // Check to see if a file with same contentHash exist $file = id(new PhabricatorFile())->loadOneWhere( 'contentHash = %s LIMIT 1', $hash); if ($file) { // copy storageEngine, storageHandle, storageFormat $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); $copy_of_byteSize = $file->getByteSize(); $copy_of_mimeType = $file->getMimeType(); $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); $file_ttl = idx($params, 'ttl'); $authorPHID = idx($params, 'authorPHID'); $new_file = new PhabricatorFile(); $new_file->setName($file_name); $new_file->setByteSize($copy_of_byteSize); $new_file->setAuthorPHID($authorPHID); $new_file->setTtl($file_ttl); $new_file->setContentHash($hash); $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); $new_file->setStorageFormat($copy_of_storage_format); $new_file->setMimeType($copy_of_mimeType); + $new_file->copyDimensions($file); $new_file->save(); return $new_file; } return $file; } private static function buildFromFileData($data, array $params = array()) { $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector'); if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { $selector = PhabricatorEnv::newObjectFromConfig( 'storage.engine-selector'); $engines = $selector->selectStorageEngines($data, $params); } assert_instances_of($engines, 'PhabricatorFileStorageEngine'); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $file = new PhabricatorFile(); $data_handle = null; $engine_identifier = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($engine); try { list($engine_identifier, $data_handle) = $file->writeToEngine( $engine, $data, $params); // We stored the file somewhere so stop trying to write it to other // places. break; } catch (PhabricatorFileStorageConfigurationException $ex) { // If an engine is outright misconfigured (or misimplemented), raise // that immediately since it probably needs attention. throw $ex; } catch (Exception $ex) { phlog($ex); // If an engine doesn't work, keep trying all the other valid engines // in case something else works. $exceptions[$engine_class] = $ex; } } if (!$data_handle) { throw new PhutilAggregateException( "All storage engines failed to write file:", $exceptions); } $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); $file_ttl = idx($params, 'ttl'); // If for whatever reason, authorPHID isn't passed as a param // (always the case with newFromFileDownload()), store a '' $authorPHID = idx($params, 'authorPHID'); $file->setName($file_name); $file->setByteSize(strlen($data)); $file->setAuthorPHID($authorPHID); $file->setTtl($file_ttl); $file->setContentHash(self::hashFileContent($data)); $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 { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); } try { $file->updateDimensions(false); } catch (Exception $ex) { // Do nothing } $file->save(); return $file; } 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); } public function migrateToEngine(PhabricatorFileStorageEngine $engine) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( "You can not migrate a file which hasn't yet been saved."); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); list($new_identifier, $new_handle) = $this->writeToEngine( $engine, $data, $params); $old_engine = $this->instantiateStorageEngine(); $old_handle = $this->getStorageHandle(); $this->setStorageEngine($new_identifier); $this->setStorageHandle($new_handle); $this->save(); $old_engine->deleteFile($old_handle); return $this; } private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, array $params) { $engine_class = get_class($engine); $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' executed writeFile() but did ". "not return a valid handle ('{$data_handle}') to the data: it ". "must be nonempty and no longer than 255 characters."); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } return array($engine_identifier, $data_handle); } public static function newFromFileDownload($uri, array $params = array()) { // Make sure we're allowed to make a request first if (!PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) { throw new Exception("Outbound HTTP requests are disabled!"); } $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 = 5; list($file_data) = id(new HTTPSFuture($uri)) ->setTimeout($timeout) ->resolvex(); $params = $params + array( 'name' => basename($uri), ); return self::newFromFileData($file_data, $params); } public static function normalizeFileName($file_name) { return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name); } public function delete() { // delete all records of this file in transformedfile $trans_files = id(new PhabricatorTransformedFile())->loadAllWhere( 'TransformedPHID = %s', $this->getPHID()); $this->openTransaction(); foreach ($trans_files as $trans_file) { $trans_file->delete(); } $ret = parent::delete(); $this->saveTransaction(); // Check to see if other files are using storage $other_file = id(new PhabricatorFile())->loadAllWhere( 'storageEngine = %s AND storageHandle = %s AND storageFormat = %s AND id != %d LIMIT 1', $this->getStorageEngine(), $this->getStorageHandle(), $this->getStorageFormat(), $this->getID()); // If this is the only file using the storage, delete storage if (count($other_file) == 0) { $engine = $this->instantiateStorageEngine(); $engine->deleteFile($this->getStorageHandle()); } return $ret; } public static function hashFileContent($data) { return sha1($data); } 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() { if (!$this->getPHID()) { throw new Exception( "You must save a file before you can generate a view URI."); } $name = phutil_escape_uri($this->getName()); $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name; return PhabricatorEnv::getCDNURI($path); } public function getInfoURI() { return '/file/info/'.$this->getPHID().'/'; } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getDownloadURI() { $uri = id(new PhutilURI($this->getViewURI())) ->setQueryParam('download', true); return (string) $uri; } public function getThumb60x45URI() { $path = '/file/xform/thumb-60x45/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb160x120URI() { $path = '/file/xform/thumb-160x120/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getPreview140URI() { $path = '/file/xform/preview-140/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getPreview220URI() { $path = '/file/xform/preview-220/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb220x165URI() { $path = '/file/xform/thumb-220x165/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb280x210URI() { $path = '/file/xform/thumb-280x210/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } 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); } 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.'); } } 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; } protected function instantiateStorageEngine() { 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() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setConcreteOnly(true) ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); $results = array(); foreach ($engines as $engine_class) { $results[] = newv($engine_class['name'], array()); } return $results; } 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 getDisplayIconForMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type, 'docs_file'); } public function validateSecretKey($key) { return ($key == $this->getSecretKey()); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } return parent::save(); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception( "This file is not a viewable image."); } if (!function_exists("imagecreatefromstring")) { throw new Exception( "Cannot retrieve image information."); } $data = $this->loadFileData(); $img = imagecreatefromstring($data); if ($img === false) { throw new Exception( "Error when decoding image."); } $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); if ($save) { $this->save(); } return $this; } + 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; + } + public static function getMetadataName($metadata) { switch ($metadata) { case self::METADATA_IMAGE_WIDTH: $name = pht('Width'); break; case self::METADATA_IMAGE_HEIGHT: $name = pht('Height'); break; default: $name = ucfirst($metadata); break; } return $name; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // TODO: Implement proper per-object policies. return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/webroot/rsrc/css/application/conpherence/widget-pane.css b/webroot/rsrc/css/application/conpherence/widget-pane.css index 97a6b19e58..f793eca9b7 100644 --- a/webroot/rsrc/css/application/conpherence/widget-pane.css +++ b/webroot/rsrc/css/application/conpherence/widget-pane.css @@ -1,174 +1,176 @@ /** * @provides conpherence-widget-pane-css */ .conpherence-widget-pane { position: fixed; right: 0px; top: 124px; width: 320px; height: 100%; border-width: 0 0 0 1px; border-color: #CCC; border-style: solid; background: url('/rsrc/image/texture/dust_background.jpg'); overflow-y: auto; } .conpherence-widget-pane .aphront-form-input { margin: 0; width: 100%; } .conpherence-widget-pane .widgets-header { background-color: #d8dce2; width: 320px; box-shadow: 0px 2px 2px rgba(0,0,0,0.15); } .conpherence-widget-pane .widgets-header .widgets-header-icon-holder { height: 40px; } .device-desktop .conpherence-widget-pane .widgets-header .widgets-header-icon-holder { width: 196px; margin: 0px auto 0px auto; } .conpherence-widget-pane .widgets-header .sprite-conpherence { display: block; width: 29px; height: 34px; margin: 4px 0px 0px 20px; float: left; clear: none; } .conpherence-widget-pane .widgets-header .conpherence_list_on, .conpherence-widget-pane .widgets-header .conpherence_conversation_on, .conpherence-widget-pane .widgets-header .conpherence_people_on, .conpherence-widget-pane .widgets-header .conpherence_files_on, .conpherence-widget-pane .widgets-header .conpherence_calendar_on, .conpherence-widget-pane .widgets-header .conpherence_settings_on { border-bottom: 3px solid #525252; } .device-desktop .conpherence-widget-pane .widgets-header #widgets-conpherence-list-toggle, .device-desktop .conpherence-widget-pane .widgets-header #widgets-conpherence-conversation-toggle { display: none; } .conpherence-widget-pane .widgets-body { position: fixed; overflow-y: auto; top: 165px; bottom: 0px; width: 320px; } /* files widget */ .conpherence-widget-pane #widgets-files .file-entry { padding: 12px 0px 14px 0px; width: 320px; } .conpherence-widget-pane #widgets-files .file-icon { position: relative; top: 0px; left: 8px; width: 32px; height: 32px; float: left; } .conpherence-widget-pane #widgets-files .file-title { position: relative; top: -4px; left: 20px; + overflow-x: hidden; + width: 270px; font-weight: bold; } .conpherence-widget-pane #widgets-files .file-uploaded-by { color: #bfbfbf; position: relative; top: 0px; left: 20px; width: 270px; font-size: 11px; } .conpherence-widget-pane #widgets-files .file-extra { display: block; height: 120px; width: 160px; margin: 8px 0px 8px 52px; border: 1px solid rgb(24, 85, 157); } .conpherence-widget-pane #widgets-files .divider { float: left; clear: both; width: 242px; margin: 8px 0px 0px 52px; border: 1px dashed #bfbfbf; } /* calendar widget */ .conpherence-widget-pane #widgets-calendar .user-status { height: 60px; } .conpherence-widget-pane #widgets-calendar .user-status .icon { border-radius: 10px; position: relative; top: 24px; left: 12px; height: 16px; width: 16px; box-shadow: 0px 0px 1px #000; } .conpherence-widget-pane #widgets-calendar .sporadic .icon { background-color: rgb(222, 226, 232); } .conpherence-widget-pane #widgets-calendar .away .icon { background-color: rgb(102, 204, 255); } .conpherence-widget-pane #widgets-calendar .user-status .epoch-range { float: right; font-style: italic; position: relative; top: 24px; right: 8px; font-size: 11px; } .conpherence-widget-pane #widgets-calendar .user-status .description { position: relative; left: 40px; top: 0px; width: 260px; } .conpherence-widget-pane #widgets-calendar .user-status .participant { position: relative; left: 40px; top: 0px; font-style: italic; font-size: 11px; width: 260px; } .conpherence-widget-pane .widget-icon { display: block; height: 14px; width: 14px; } .conpherence-widget-pane .phabricator-remarkup-embed-layout-link { padding-bottom: 1px; }