diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 43a8135354..07b12b7172 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,640 +1,640 @@ <?php /* * Copyright 2012 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. */ /** * @group aphront */ class AphrontDefaultApplicationConfiguration extends AphrontApplicationConfiguration { public function __construct() { } public function getApplicationName() { return 'aphront-default'; } public function getURIMap() { return $this->getResourceURIMapRules() + array( '/(?:(?P<filter>jump)/)?$' => 'PhabricatorDirectoryMainController', '/(?:(?P<filter>feed)/)' => array( 'public/$' => 'PhabricatorFeedPublicStreamController', '(?:(?P<subfilter>[^/]+)/)?$' => 'PhabricatorDirectoryMainController', ), '/directory/' => array( '(?P<id>\d+)/$' => 'PhabricatorDirectoryCategoryViewController', 'edit/$' => 'PhabricatorDirectoryEditController', 'item/edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorDirectoryItemEditController', 'item/delete/(?P<id>\d+)/' => 'PhabricatorDirectoryItemDeleteController', 'category/edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorDirectoryCategoryEditController', 'category/delete/(?P<id>\d+)/' => 'PhabricatorDirectoryCategoryDeleteController', ), '/file/' => array( '$' => 'PhabricatorFileListController', 'filter/(?P<filter>\w+)/$' => 'PhabricatorFileListController', 'upload/$' => 'PhabricatorFileUploadController', 'dropupload/$' => 'PhabricatorFileDropUploadController', 'delete/(?P<id>\d+)/$' => 'PhabricatorFileDeleteController', 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController', 'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/' => 'PhabricatorFileDataController', // TODO: This is a deprecated version of /data/. Remove it after // old links have had a chance to rot. 'alt/(?P<key>[^/]+)/(?P<phid>[^/]+)/' => 'PhabricatorFileDataController', 'macro/' => array( '$' => 'PhabricatorFileMacroListController', 'edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorFileMacroEditController', 'delete/(?P<id>\d+)/$' => 'PhabricatorFileMacroDeleteController', ), 'proxy/$' => 'PhabricatorFileProxyController', 'xform/(?P<transform>[^/]+)/(?P<phid>[^/]+)/' => 'PhabricatorFileTransformController', ), '/phid/' => array( '$' => 'PhabricatorPHIDLookupController', ), '/people/' => array( '$' => 'PhabricatorPeopleListController', 'logs/$' => 'PhabricatorPeopleLogsController', 'edit/(?:(?P<id>\d+)/(?:(?P<view>\w+)/)?)?$' => 'PhabricatorPeopleEditController', ), '/p/(?P<username>\w+)/(?:(?P<page>\w+)/)?$' => 'PhabricatorPeopleProfileController', '/conduit/' => array( '$' => 'PhabricatorConduitConsoleController', 'method/(?P<method>[^/]+)/$' => 'PhabricatorConduitConsoleController', 'log/$' => 'PhabricatorConduitLogController', 'log/view/(?P<view>[^/]+)/$' => 'PhabricatorConduitLogController', 'token/$' => 'PhabricatorConduitTokenController', ), '/api/(?P<method>[^/]+)$' => 'PhabricatorConduitAPIController', '/D(?P<id>\d+)' => 'DifferentialRevisionViewController', '/differential/' => array( '$' => 'DifferentialRevisionListController', 'filter/(?P<filter>\w+)/$' => 'DifferentialRevisionListController', 'stats/(?P<filter>\w+)/$' => 'DifferentialRevisionStatsController', 'diff/' => array( '(?P<id>\d+)/$' => 'DifferentialDiffViewController', 'create/$' => 'DifferentialDiffCreateController', ), 'changeset/$' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P<id>\d+)/)?$' => 'DifferentialRevisionEditController', 'comment/' => array( 'preview/(?P<id>\d+)/$' => 'DifferentialCommentPreviewController', 'save/$' => 'DifferentialCommentSaveController', 'inline/' => array( 'preview/(?P<id>\d+)/$' => 'DifferentialInlineCommentPreviewController', 'edit/(?P<id>\d+)/$' => 'DifferentialInlineCommentEditController', ), ), 'subscribe/(?P<action>add|rem)/(?P<id>\d+)/$' => 'DifferentialSubscribeController', ), '/typeahead/' => array( 'common/(?P<type>\w+)/$' => 'PhabricatorTypeaheadCommonDatasourceController', ), '/mail/' => array( '$' => 'PhabricatorMetaMTAListController', 'send/$' => 'PhabricatorMetaMTASendController', 'view/(?P<id>\d+)/$' => 'PhabricatorMetaMTAViewController', 'lists/$' => 'PhabricatorMetaMTAMailingListsController', 'lists/edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorMetaMTAMailingListEditController', 'receive/$' => 'PhabricatorMetaMTAReceiveController', 'received/$' => 'PhabricatorMetaMTAReceivedListController', 'sendgrid/$' => 'PhabricatorMetaMTASendGridReceiveController', ), '/login/' => array( '$' => 'PhabricatorLoginController', 'email/$' => 'PhabricatorEmailLoginController', 'etoken/(?P<token>\w+)/$' => 'PhabricatorEmailTokenController', 'refresh/$' => 'PhabricatorRefreshCSRFController', 'validate/$' => 'PhabricatorLoginValidateController', ), '/logout/$' => 'PhabricatorLogoutController', '/oauth/' => array( '(?P<provider>\w+)/' => array( 'login/$' => 'PhabricatorOAuthLoginController', 'diagnose/$' => 'PhabricatorOAuthDiagnosticsController', 'unlink/$' => 'PhabricatorOAuthUnlinkController', ), ), '/oauthserver/' => array( 'auth/' => 'PhabricatorOAuthServerAuthController', 'test/' => 'PhabricatorOAuthServerTestController', 'token/' => 'PhabricatorOAuthServerTokenController', 'clientauthorization/' => array( '$' => 'PhabricatorOAuthClientAuthorizationListController', 'delete/(?P<phid>[^/]+)/' => 'PhabricatorOAuthClientAuthorizationDeleteController', 'edit/(?P<phid>[^/]+)/' => 'PhabricatorOAuthClientAuthorizationEditController', ), 'client/' => array( '$' => 'PhabricatorOAuthClientListController', 'create/$' => 'PhabricatorOAuthClientEditController', 'delete/(?P<phid>[^/]+)/$' => 'PhabricatorOAuthClientDeleteController', 'edit/(?P<phid>[^/]+)/$' => 'PhabricatorOAuthClientEditController', 'view/(?P<phid>[^/]+)/$' => 'PhabricatorOAuthClientViewController', ), ), '/xhprof/' => array( 'profile/(?P<phid>[^/]+)/$' => 'PhabricatorXHProfProfileController', ), '/~/' => 'DarkConsoleController', '/settings/' => array( '(?:page/(?P<page>[^/]+)/)?$' => 'PhabricatorUserSettingsController', ), '/maniphest/' => array( '$' => 'ManiphestTaskListController', 'view/(?P<view>\w+)/$' => 'ManiphestTaskListController', 'report/(?:(?P<view>\w+)/)?$' => 'ManiphestReportController', 'batch/$' => 'ManiphestBatchEditController', 'task/' => array( 'create/$' => 'ManiphestTaskEditController', 'edit/(?P<id>\d+)/$' => 'ManiphestTaskEditController', 'descriptionchange/(?P<id>\d+)/$' => 'ManiphestTaskDescriptionChangeController', 'descriptiondiff/$' => 'ManiphestTaskDescriptionDiffController', 'descriptionpreview/$' => 'ManiphestTaskDescriptionPreviewController', ), 'transaction/' => array( 'save/' => 'ManiphestTransactionSaveController', 'preview/(?P<id>\d+)/$' => 'ManiphestTransactionPreviewController', ), ), '/T(?P<id>\d+)$' => 'ManiphestTaskDetailController', '/repository/' => array( '$' => 'PhabricatorRepositoryListController', 'create/$' => 'PhabricatorRepositoryCreateController', 'edit/(?P<id>\d+)/(?:(?P<view>\w+)?/)?$' => 'PhabricatorRepositoryEditController', 'delete/(?P<id>\d+)/$' => 'PhabricatorRepositoryDeleteController', 'project/(?P<id>\d+)/' => 'PhabricatorRepositoryArcanistProjectEditController', ), '/search/' => array( '$' => 'PhabricatorSearchController', '(?P<key>[^/]+)/$' => 'PhabricatorSearchController', 'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?$' => 'PhabricatorSearchAttachController', 'select/(?P<type>\w+)/$' => 'PhabricatorSearchSelectController', 'index/(?P<phid>[^/]+)/$' => 'PhabricatorSearchIndexController', ), '/project/' => array( '$' => 'PhabricatorProjectListController', 'filter/(?P<filter>[^/]+)/$' => 'PhabricatorProjectListController', 'edit/(?P<id>\d+)/$' => 'PhabricatorProjectProfileEditController', 'view/(?P<id>\d+)/(?:(?P<page>\w+)/)?$' => 'PhabricatorProjectProfileController', 'create/$' => 'PhabricatorProjectCreateController', 'update/(?P<id>\d+)/(?P<action>[^/]+)/$' => 'PhabricatorProjectUpdateController', ), '/r(?P<callsign>[A-Z]+)(?P<commit>[a-z0-9]+)$' => 'DiffusionCommitController', '/diffusion/' => array( '$' => 'DiffusionHomeController', '(?P<callsign>[A-Z]+)/' => array( '$' => 'DiffusionRepositoryController', 'repository/'. '(?P<path>[^/]+)/'. '$' => 'DiffusionRepositoryController', 'change/'. '(?P<path>.*?)'. '(?:[;](?P<commit>[a-z0-9]+))?'. '$' => 'DiffusionChangeController', 'history/'. '(?P<path>.*?)'. '(?:[;](?P<commit>[a-z0-9]+))?'. '$' => 'DiffusionHistoryController', 'browse/'. '(?P<path>.*?)'. '(?:[;](?P<commit>[a-z0-9]+))?'. - '(?:[$](?P<line>\d+))?'. + '(?:[$](?P<line>\d+(?:-\d+)?))?'. '$' => 'DiffusionBrowseController', 'diff/'. '(?P<path>.*?)'. '(?:[;](?P<commit>[a-z0-9]+))?'. '$' => 'DiffusionDiffController', 'lastmodified/'. '(?P<path>.*?)'. '(?:[;](?P<commit>[a-z0-9]+))?'. '$' => 'DiffusionLastModifiedController', ), 'services/' => array( 'path/' => array( 'complete/$' => 'DiffusionPathCompleteController', 'validate/$' => 'DiffusionPathValidateController', ), ), 'author/' => array( '$' => 'DiffusionCommitListController', '(?P<username>\w+)/$' => 'DiffusionCommitListController', ), 'symbol/(?P<name>[^/]+)/$' => 'DiffusionSymbolController', ), '/daemon/' => array( 'task/(?P<id>\d+)/$' => 'PhabricatorWorkerTaskDetailController', 'task/(?P<id>\d+)/(?P<action>[^/]+)/$' => 'PhabricatorWorkerTaskUpdateController', 'log/' => array( '$' => 'PhabricatorDaemonLogListController', 'combined/$' => 'PhabricatorDaemonCombinedLogController', '(?P<id>\d+)/$' => 'PhabricatorDaemonLogViewController', ), 'timeline/$' => 'PhabricatorDaemonTimelineConsoleController', 'timeline/(?P<id>\d+)/$' => 'PhabricatorDaemonTimelineEventController', '$' => 'PhabricatorDaemonConsoleController', ), '/herald/' => array( '$' => 'HeraldHomeController', 'view/(?P<view>[^/]+)/' => array( '$' => 'HeraldHomeController', '(?P<global>global)/$' => 'HeraldHomeController' ), 'new/(?:(?P<type>[^/]+)/)?$' => 'HeraldNewController', 'rule/(?:(?P<id>\d+)/)?$' => 'HeraldRuleController', 'history/(?P<id>\d+)/$' => 'HeraldRuleEditHistoryController', 'delete/(?P<id>\d+)/$' => 'HeraldDeleteController', 'test/$' => 'HeraldTestConsoleController', 'all/' => array( '$' => 'HeraldAllRulesController', 'view/(?P<view>[^/]+)/$' => 'HeraldAllRulesController', ), 'transcript/$' => 'HeraldTranscriptListController', 'transcript/(?P<id>\d+)/(?:(?P<filter>\w+)/)?$' => 'HeraldTranscriptController', ), '/uiexample/' => array( '$' => 'PhabricatorUIExampleRenderController', 'view/(?P<class>[^/]+)/$' => 'PhabricatorUIExampleRenderController', ), '/owners/' => array( '$' => 'PhabricatorOwnersListController', 'view/(?P<view>[^/]+)/$' => 'PhabricatorOwnersListController', 'edit/(?P<id>\d+)/$' => 'PhabricatorOwnersEditController', 'new/$' => 'PhabricatorOwnersEditController', 'package/(?P<id>\d+)/$' => 'PhabricatorOwnersDetailController', 'delete/(?P<id>\d+)/$' => 'PhabricatorOwnersDeleteController', '(?P<scope>related|attention)/' => array( '$' => 'PhabricatorOwnerRelatedListController', '(?P<view>package|owner)/$' => 'PhabricatorOwnerRelatedListController', ), ), '/audit/' => array( '$' => 'PhabricatorAuditListController', 'view/(?P<filter>[^/]+)/$' => 'PhabricatorAuditListController', 'edit/$' => 'PhabricatorAuditEditController', 'addcomment/$' => 'PhabricatorAuditAddCommentController', ), '/xhpast/' => array( '$' => 'PhabricatorXHPASTViewRunController', 'view/(?P<id>\d+)/$' => 'PhabricatorXHPASTViewFrameController', 'frameset/(?P<id>\d+)/$' => 'PhabricatorXHPASTViewFramesetController', 'input/(?P<id>\d+)/$' => 'PhabricatorXHPASTViewInputController', 'tree/(?P<id>\d+)/$' => 'PhabricatorXHPASTViewTreeController', 'stream/(?P<id>\d+)/$' => 'PhabricatorXHPASTViewStreamController', ), '/status/$' => 'PhabricatorStatusController', '/paste/' => array( '$' => 'PhabricatorPasteListController', 'filter/(?P<filter>\w+)/$' => 'PhabricatorPasteListController', ), '/P(?P<id>\d+)$' => 'PhabricatorPasteViewController', '/help/' => array( 'keyboardshortcut/$' => 'PhabricatorHelpKeyboardShortcutController', ), '/countdown/' => array( '$' => 'PhabricatorCountdownListController', '(?P<id>\d+)/$' => 'PhabricatorCountdownViewController', 'edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorCountdownEditController', 'delete/(?P<id>\d+)/$' => 'PhabricatorCountdownDeleteController' ), '/V(?P<id>\d+)$' => 'PhabricatorSlowvotePollController', '/vote/' => array( '(?:view/(?P<view>\w+)/)?$' => 'PhabricatorSlowvoteListController', 'create/' => 'PhabricatorSlowvoteCreateController', ), // Match "/w/" with slug "/". '/w(?P<slug>/)$' => 'PhrictionDocumentController', // Match "/w/x/y/z/" with slug "x/y/z/". '/w/(?P<slug>.+/)$' => 'PhrictionDocumentController', '/phriction/' => array( '$' => 'PhrictionListController', 'list/(?P<view>[^/]+)/$' => 'PhrictionListController', 'history(?P<slug>/)$' => 'PhrictionHistoryController', 'history/(?P<slug>.+/)$' => 'PhrictionHistoryController', 'edit/(?:(?P<id>\d+)/)?$' => 'PhrictionEditController', 'delete/(?P<id>\d+)/$' => 'PhrictionDeleteController', 'preview/$' => 'PhrictionDocumentPreviewController', 'diff/(?P<id>\d+)/$' => 'PhrictionDiffController', ), '/calendar/' => array( '$' => 'PhabricatorCalendarBrowseController', ), '/drydock/' => array( '$' => 'DrydockResourceListController', 'resource/$' => 'DrydockResourceListController', 'resource/allocate/$' => 'DrydockResourceAllocateController', 'host/' => array( '$' => 'DrydockHostListController', 'edit/$' => 'DrydockHostEditController', 'edit/(?P<id>\d+)/$' => 'DrydockhostEditController', ), 'lease/$' => 'DrydockLeaseListController', ), '/chatlog/' => array( '$' => 'PhabricatorChatLogChannelListController', 'channel/(?P<channel>[^/]+)/$' => 'PhabricatorChatLogChannelLogController', ), ); } protected function getResourceURIMapRules() { return array( '/res/' => array( '(?P<package>pkg/)?(?P<hash>[a-f0-9]{8})/(?P<path>.+\.(?:css|js))$' => 'CelerityResourceController', ), ); } public function buildRequest() { $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($_GET + $_POST); $request->setApplicationConfiguration($this); return $request; } public function handleException(Exception $ex) { // Always log the unhandled exception. phlog($ex); $class = phutil_escape_html(get_class($ex)); $message = phutil_escape_html($ex->getMessage()); if (PhabricatorEnv::getEnvConfig('phabricator.show-stack-traces')) { $trace = $this->renderStackTrace($ex->getTrace()); } else { $trace = null; } $content = '<div class="aphront-unhandled-exception">'. '<div class="exception-message">'.$message.'</div>'. $trace. '</div>'; $user = $this->getRequest()->getUser(); if (!$user) { // If we hit an exception very early, we won't have a user. $user = new PhabricatorUser(); } $dialog = new AphrontDialogView(); $dialog ->setTitle('Unhandled Exception ("'.$class.'")') ->setClass('aphront-exception-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } public function willSendResponse(AphrontResponse $response) { $request = $this->getRequest(); $response->setRequest($request); if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->appendChild( '<div style="padding: 2em 0;">'. $response->buildResponseString(). '</div>'); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } else { return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } public function buildRedirectController($uri) { return array( new PhabricatorRedirectController($this->getRequest()), array( 'uri' => $uri, )); } private function renderStackTrace($trace) { $libraries = PhutilBootloader::getInstance()->getAllLibraries(); // TODO: Make this configurable? $host = 'https://secure.phabricator.com'; $browse = array( 'arcanist' => $host.'/diffusion/ARC/browse/origin:master/src/', 'phutil' => $host.'/diffusion/PHU/browse/origin:master/src/', 'phabricator' => $host.'/diffusion/P/browse/origin:master/src/', ); $rows = array(); $depth = count($trace); foreach ($trace as $part) { $lib = null; $file = idx($part, 'file'); $relative = $file; foreach ($libraries as $library) { $root = phutil_get_library_root($library); if (Filesystem::isDescendant($file, $root)) { $lib = $library; $relative = Filesystem::readablePath($file, $root); break; } } $where = ''; if (isset($part['class'])) { $where .= $part['class'].'::'; } if (isset($part['function'])) { $where .= $part['function'].'()'; } if ($file) { if (isset($browse[$lib])) { $file_name = phutil_render_tag( 'a', array( 'href' => $browse[$lib].$relative.'$'.$part['line'], 'title' => $file, 'target' => '_blank', ), phutil_escape_html($relative)); } else { $file_name = phutil_render_tag( 'span', array( 'title' => $file, ), phutil_escape_html($relative)); } $file_name = $file_name.' : '.(int)$part['line']; } else { $file_name = '<em>(Internal)</em>'; } $rows[] = array( $depth--, phutil_escape_html($lib), $file_name, phutil_escape_html($where), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Depth', 'Library', 'File', 'Where', )); $table->setColumnClasses( array( 'n', '', '', 'wide', )); return '<div class="exception-trace">'. '<div class="exception-trace-header">Stack Trace</div>'. $table->render(). '</div>'; } } diff --git a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php index 40907f71fd..bd1bf2e35e 100644 --- a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php @@ -1,461 +1,476 @@ <?php /* * Copyright 2012 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 DiffusionBrowseFileController extends DiffusionController { // Image types we want to display inline using <img> tags protected $imageTypes = array( 'png' => 'image/png', 'gif' => 'image/gif', 'ico' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg'=> 'image/jpeg' ); // Document types that should trigger link to ?view=raw protected $documentTypes = array( 'pdf'=> 'application/pdf', 'ps' => 'application/postscript', ); public function processRequest() { // Build the view selection form. $select_map = array( 'highlighted' => 'View as Highlighted Text', 'blame' => 'View as Highlighted Text with Blame', 'plain' => 'View as Plain Text', 'plainblame' => 'View as Plain Text with Blame', 'raw' => 'View as raw document', ); $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $selected = $request->getStr('view'); $needs_blame = ($selected == 'blame' || $selected == 'plainblame'); $file_query = DiffusionFileContentQuery::newFromDiffusionRequest( $this->diffusionRequest); $file_query->setNeedsBlame($needs_blame); $file_query->loadFileContent(); $data = $file_query->getRawData(); if ($selected === 'raw') { $response = new AphrontFileResponse(); $response->setContent($data); $mime_type = $this->getDocumentType($path); if ($mime_type) { $response->setMimeType($mime_type); } else { $as_filename = idx(pathinfo($path), 'basename'); $response->setDownload($as_filename); } return $response; } $select = '<select name="view">'; foreach ($select_map as $k => $v) { $option = phutil_render_tag( 'option', array( 'value' => $k, 'selected' => ($k == $selected) ? 'selected' : null, ), phutil_escape_html($v)); $select .= $option; } $select .= '</select>'; require_celerity_resource('diffusion-source-css'); $view_select_panel = new AphrontPanelView(); $view_select_form = phutil_render_tag( 'form', array( 'action' => $request->getRequestURI(), 'method' => 'get', 'class' => 'diffusion-browse-type-form', ), $select. '<button>View</button>'); $view_select_panel->appendChild($view_select_form); $user = $request->getUser(); if ($user) { $line = 1; $repository = $this->getDiffusionRequest()->getRepository(); $editor_link = $user->loadEditorLink($path, $line, $repository); if ($editor_link) { $view_select_panel->addButton( phutil_render_tag( 'a', array( 'href' => $editor_link, 'class' => 'button', ), 'Edit' )); } } $view_select_panel->appendChild('<div style="clear: both;"></div>'); // Build the content of the file. $corpus = $this->buildCorpus( $selected, $file_query, $needs_blame, $drequest, $path, $data ); // Render the page. $content = array(); $content[] = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $content[] = $view_select_panel; $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $nav = $this->buildSideNav('browse', true); $nav->appendChild($content); $basename = basename($this->getDiffusionRequest()->getPath()); return $this->buildStandardPageResponse( $nav, array( 'title' => $basename, )); } /** * Returns a content-type corrsponding to an image file extension * * @param string $path File path * @return mixed A content-type string or NULL if path doesn't end with a * recognized image extension */ public function getImageType($path) { $ext = pathinfo($path); $ext = idx($ext, 'extension'); return idx($this->imageTypes, $ext); } /** * Returns a content-type corresponding to an document file extension * * @param string $path File path * @return mixed A content-type string or NULL if path doesn't end with a * recognized document extension */ public function getDocumentType($path) { $ext = pathinfo($path); $ext = idx($ext, 'extension'); return idx($this->documentTypes, $ext); } private function buildCorpus($selected, $file_query, $needs_blame, $drequest, $path, $data) { $image_type = $this->getImageType($path); if ($image_type && !$selected) { $corpus = phutil_render_tag( 'img', array( 'style' => 'padding-bottom: 10px', 'src' => 'data:'.$image_type.';base64,'.base64_encode($data), ) ); return $corpus; } $document_type = $this->getDocumentType($path); if (($document_type && !$selected) || !phutil_is_utf8($data)) { $data = $file_query->getRawData(); $document_type_description = $document_type ? $document_type : 'binary'; $corpus = phutil_render_tag( 'p', array( 'style' => 'text-align: center;' ), phutil_render_tag( 'a', array( 'href' => '?view=raw', 'class' => 'button' ), "View $document_type_description" ) ); return $corpus; } // TODO: blame of blame. switch ($selected) { case 'plain': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; $corpus = phutil_render_tag( 'textarea', array( 'style' => $style, ), phutil_escape_html($file_query->getRawData())); break; case 'plainblame': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $rows = array(); foreach ($text_list as $k => $line) { $rev = $rev_list[$k]; if (isset($blame_dict[$rev]['handle'])) { $author = $blame_dict[$rev]['handle']->getName(); } else { $author = $blame_dict[$rev]['author']; } $rows[] = sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line); } $corpus = phutil_render_tag( 'textarea', array( 'style' => $style, ), phutil_escape_html(implode("\n", $rows))); break; case 'highlighted': case 'blame': default: require_celerity_resource('syntax-highlighting-css'); list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $text_list = implode("\n", $text_list); $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $text_list); $text_list = explode("\n", $text_list); $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict, $needs_blame, $drequest, $file_query, $selected); $corpus_table = phutil_render_tag( 'table', array( 'class' => "diffusion-source remarkup-code PhabricatorMonospaced", ), implode("\n", $rows)); $corpus = phutil_render_tag( 'div', array( 'style' => 'padding: 0pt 2em;', ), $corpus_table); break; } return $corpus; } private function buildDisplayRows($text_list, $rev_list, $blame_dict, $needs_blame, DiffusionRequest $drequest, $file_query, $selected) { $last_rev = null; $color = null; $rows = array(); $n = 1; $view = $this->getRequest()->getStr('view'); if ($blame_dict) { $epoch_list = ipull($blame_dict, 'epoch'); $max = max($epoch_list); $min = min($epoch_list); $range = $max - $min + 1; } else { $range = 1; } + $targ = ''; + $min_line = 0; + $line = $drequest->getLine(); + if (strpos($line,'-') !== false) { + list($min,$max) = explode('-',$line,2); + $min_line = min($min, $max); + $max_line = max($min, $max); + } else if (strlen($line)) { + $min_line = $line; + $max_line = $line; + } + + foreach ($text_list as $k => $line) { if ($needs_blame) { // If the line's rev is same as the line above, show empty content // with same color; otherwise generate blame info. The newer a change // is, the darker the color. $rev = $rev_list[$k]; if ($last_rev == $rev) { $blame_info = ($file_query->getSupportsBlameOnBlame() ? '<th style="background: '.$color.'; width: 2em;"></th>' : ''). '<th style="background: '.$color.'; width: 9em;"></th>'. '<th style="background: '.$color.'"></th>'; } else { $color_number = (int)(0xEE - 0xEE * ($blame_dict[$rev]['epoch'] - $min) / $range); $color = sprintf('#%02xee%02x', $color_number, $color_number); $revision_link = self::renderRevision( $drequest, substr($rev, 0, 7)); if (!$file_query->getSupportsBlameOnBlame()) { $prev_link = ''; } else { $prev_rev = $file_query->getPrevRev($rev); $path = $drequest->getPath(); $prev_link = self::renderBrowse( $drequest, $path, "\xC2\xAB", $prev_rev, $n, $selected, 'Blame previous revision'); $prev_link = '<th style="background: ' . $color . '; width: 2em;">' . $prev_link . '</th>'; } if (isset($blame_dict[$rev]['handle'])) { $author_link = $blame_dict[$rev]['handle']->renderLink(); } else { $author_link = phutil_escape_html($blame_dict[$rev]['author']); } $blame_info = $prev_link . '<th style="background: '.$color. '; width: 12em;">'.$revision_link.'</th>'. '<th style="background: '.$color.'; width: 12em'. '; font-weight: normal; color: #333;">'.$author_link.'</th>'; $last_rev = $rev; } } else { $blame_info = null; } // Highlight the line of interest if needed. - if ($n == $drequest->getLine()) { + if ($min_line > 0 && ($n >= $min_line && $n <= $max_line)) { $tr = '<tr style="background: #ffff00;">'; - $targ = '<a id="scroll_target"></a>'; - Javelin::initBehavior('diffusion-jump-to', - array('target' => 'scroll_target')); + if ($targ == '') { + $targ = '<a id="scroll_target"></a>'; + Javelin::initBehavior('diffusion-jump-to', + array('target' => 'scroll_target')); + } } else { $tr = '<tr>'; $targ = null; } // Create the row display. $uri_path = $drequest->getUriPath(); $uri_rev = $drequest->getStableCommitName(); $uri_view = $view ? '?view='.$view : null; $l = phutil_render_tag( 'a', array( 'class' => 'diffusion-line-link', 'href' => $uri_path.';'.$uri_rev.'$'.$n.$uri_view, ), $n); $rows[] = $tr.$blame_info.'<th>'.$l.'</th><td>'.$targ.$line.'</td></tr>'; ++$n; } return $rows; } private static function renderRevision(DiffusionRequest $drequest, $revision) { $callsign = $drequest->getCallsign(); $name = 'r'.$callsign.$revision; return phutil_render_tag( 'a', array( 'href' => '/'.$name, ), $name ); } private static function renderBrowse( DiffusionRequest $drequest, $path, $name = null, $rev = null, $line = null, $view = null, $title = null) { $callsign = $drequest->getCallsign(); if ($name === null) { $name = $path; } $at = null; if ($rev) { $at = ';'.$rev; } if ($view) { $view = '?view='.$view; } if ($line) { $line = '$'.$line; } return phutil_render_tag( 'a', array( 'href' => "/diffusion/{$callsign}/browse/{$path}{$at}{$line}{$view}", 'title' => $title, ), $name ); } }