diff --git a/externals/javelin b/externals/javelin index 748eac2b2..1cdedbfc0 160000 --- a/externals/javelin +++ b/externals/javelin @@ -1 +1 @@ -Subproject commit 748eac2b2fb3f210dc68506d2bc36f96a6b0d77f +Subproject commit 1cdedbfc00a32f3167aa3bc14dd917cf886014ff diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 3f0a55a1d..ee6b5ca26 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,691 +1,713 @@ getResourceURIMapRules() + array( '/(?:(?P(?:jump|apps))/)?' => 'PhabricatorDirectoryMainController', '/(?:(?Pfeed)/)' => array( 'public/' => 'PhabricatorFeedPublicStreamController', '(?:(?P[^/]+)/)?' => 'PhabricatorDirectoryMainController', ), '/F(?P\d+)' => 'PhabricatorFileShortcutController', '/file/' => array( '' => 'PhabricatorFileListController', 'filter/(?P\w+)/' => 'PhabricatorFileListController', 'upload/' => 'PhabricatorFileUploadController', 'dropupload/' => 'PhabricatorFileDropUploadController', 'delete/(?P\d+)/' => 'PhabricatorFileDeleteController', 'info/(?P[^/]+)/' => 'PhabricatorFileInfoController', 'data/(?P[^/]+)/(?P[^/]+)/.*' => 'PhabricatorFileDataController', // TODO: This is a deprecated version of /data/. Remove it after // old links have had a chance to rot. 'alt/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorFileDataController', 'macro/' => array( '' => 'PhabricatorFileMacroListController', 'edit/(?:(?P\d+)/)?' => 'PhabricatorFileMacroEditController', 'delete/(?P\d+)/' => 'PhabricatorFileMacroDeleteController', ), 'proxy/' => 'PhabricatorFileProxyController', 'xform/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorFileTransformController', ), '/phid/' => array( '' => 'PhabricatorPHIDLookupController', ), '/people/' => array( '' => 'PhabricatorPeopleListController', 'logs/' => 'PhabricatorPeopleLogsController', 'edit/(?:(?P\d+)/(?:(?P\w+)/)?)?' => 'PhabricatorPeopleEditController', ), '/p/(?P\w+)/(?:(?P\w+)/)?' => 'PhabricatorPeopleProfileController', '/conduit/' => array( '' => 'PhabricatorConduitListController', 'method/(?P[^/]+)/' => 'PhabricatorConduitConsoleController', 'log/' => 'PhabricatorConduitLogController', 'log/view/(?P[^/]+)/' => 'PhabricatorConduitLogController', 'token/' => 'PhabricatorConduitTokenController', ), '/api/(?P[^/]+)' => 'PhabricatorConduitAPIController', '/D(?P\d+)' => 'DifferentialRevisionViewController', '/differential/' => array( '' => 'DifferentialRevisionListController', 'filter/(?P\w+)/(?:(?P\w+)/)?' => 'DifferentialRevisionListController', 'stats/(?P\w+)/' => 'DifferentialRevisionStatsController', 'diff/' => array( '(?P\d+)/' => 'DifferentialDiffViewController', 'create/' => 'DifferentialDiffCreateController', ), 'changeset/' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P\d+)/)?' => 'DifferentialRevisionEditController', 'comment/' => array( 'preview/(?P\d+)/' => 'DifferentialCommentPreviewController', 'save/' => 'DifferentialCommentSaveController', 'inline/' => array( 'preview/(?P\d+)/' => 'DifferentialInlineCommentPreviewController', 'edit/(?P\d+)/' => 'DifferentialInlineCommentEditController', ), ), 'subscribe/(?Padd|rem)/(?P\d+)/' => 'DifferentialSubscribeController', ), '/typeahead/' => array( 'common/(?P\w+)/' => 'PhabricatorTypeaheadCommonDatasourceController', ), '/mail/' => array( '' => 'PhabricatorMetaMTAListController', 'send/' => 'PhabricatorMetaMTASendController', 'view/(?P\d+)/' => 'PhabricatorMetaMTAViewController', 'lists/' => 'PhabricatorMetaMTAMailingListsController', 'lists/edit/(?:(?P\d+)/)?' => 'PhabricatorMetaMTAMailingListEditController', 'receive/' => 'PhabricatorMetaMTAReceiveController', 'received/' => 'PhabricatorMetaMTAReceivedListController', 'sendgrid/' => 'PhabricatorMetaMTASendGridReceiveController', ), '/login/' => array( '' => 'PhabricatorLoginController', 'email/' => 'PhabricatorEmailLoginController', 'etoken/(?P\w+)/' => 'PhabricatorEmailTokenController', 'refresh/' => 'PhabricatorRefreshCSRFController', 'validate/' => 'PhabricatorLoginValidateController', ), '/logout/' => 'PhabricatorLogoutController', '/oauth/' => array( '(?P\w+)/' => array( 'login/' => 'PhabricatorOAuthLoginController', 'diagnose/' => 'PhabricatorOAuthDiagnosticsController', 'unlink/' => 'PhabricatorOAuthUnlinkController', ), ), '/oauthserver/' => array( 'auth/' => 'PhabricatorOAuthServerAuthController', 'test/' => 'PhabricatorOAuthServerTestController', 'token/' => 'PhabricatorOAuthServerTokenController', 'clientauthorization/' => array( '' => 'PhabricatorOAuthClientAuthorizationListController', 'delete/(?P[^/]+)/' => 'PhabricatorOAuthClientAuthorizationDeleteController', 'edit/(?P[^/]+)/' => 'PhabricatorOAuthClientAuthorizationEditController', ), 'client/' => array( '' => 'PhabricatorOAuthClientListController', 'create/' => 'PhabricatorOAuthClientEditController', 'delete/(?P[^/]+)/' => 'PhabricatorOAuthClientDeleteController', 'edit/(?P[^/]+)/' => 'PhabricatorOAuthClientEditController', 'view/(?P[^/]+)/' => 'PhabricatorOAuthClientViewController', ), ), '/xhprof/' => array( 'profile/(?P[^/]+)/' => 'PhabricatorXHProfProfileController', ), '/~/' => 'DarkConsoleController', '/settings/' => array( '(?:page/(?P[^/]+)/)?' => 'PhabricatorUserSettingsController', ), '/maniphest/' => array( '' => 'ManiphestTaskListController', 'view/(?P\w+)/' => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?' => 'ManiphestReportController', 'batch/' => 'ManiphestBatchEditController', 'task/' => array( 'create/' => 'ManiphestTaskEditController', 'edit/(?P\d+)/' => 'ManiphestTaskEditController', 'descriptionchange/(?:(?P\d+)/)?' => 'ManiphestTaskDescriptionChangeController', 'descriptionpreview/' => 'ManiphestTaskDescriptionPreviewController', ), 'transaction/' => array( 'save/' => 'ManiphestTransactionSaveController', 'preview/(?P\d+)/' => 'ManiphestTransactionPreviewController', ), 'export/(?P[^/]+)/' => 'ManiphestExportController', 'subpriority/' => 'ManiphestSubpriorityController', 'custom/' => array( '' => 'ManiphestSavedQueryListController', 'edit/(?:(?P\d+)/)?' => 'ManiphestSavedQueryEditController', 'delete/(?P\d+)/' => 'ManiphestSavedQueryDeleteController', ), ), '/T(?P\d+)' => 'ManiphestTaskDetailController', '/repository/' => array( '' => 'PhabricatorRepositoryListController', 'create/' => 'PhabricatorRepositoryCreateController', 'edit/(?P\d+)/(?:(?P\w+)?/)?' => 'PhabricatorRepositoryEditController', 'delete/(?P\d+)/' => 'PhabricatorRepositoryDeleteController', 'project/(?P\d+)/' => 'PhabricatorRepositoryArcanistProjectEditController', ), '/search/' => array( '' => 'PhabricatorSearchController', '(?P[^/]+)/' => 'PhabricatorSearchController', 'attach/(?P[^/]+)/(?P\w+)/(?:(?P\w+)/)?' => 'PhabricatorSearchAttachController', 'select/(?P\w+)/' => 'PhabricatorSearchSelectController', 'index/(?P[^/]+)/' => 'PhabricatorSearchIndexController', ), '/project/' => array( '' => 'PhabricatorProjectListController', 'filter/(?P[^/]+)/' => 'PhabricatorProjectListController', 'edit/(?P\d+)/' => 'PhabricatorProjectProfileEditController', 'view/(?P\d+)/(?:(?P\w+)/)?' => 'PhabricatorProjectProfileController', 'create/' => 'PhabricatorProjectCreateController', 'update/(?P\d+)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', ), '/r(?P[A-Z]+)(?P[a-z0-9]+)' => 'DiffusionCommitController', '/diffusion/' => array( '' => 'DiffusionHomeController', '(?P[A-Z]+)/' => array( '' => 'DiffusionRepositoryController', 'repository/(?P.*)' => 'DiffusionRepositoryController', 'change/(?P.*)' => 'DiffusionChangeController', 'history/(?P.*)' => 'DiffusionHistoryController', 'browse/(?P.*)' => 'DiffusionBrowseController', 'lastmodified/(?P.*)' => 'DiffusionLastModifiedController', 'diff/' => 'DiffusionDiffController', 'tags/(?P.*)' => 'DiffusionTagListController', ), 'inline/(?P[^/]+)/' => 'DiffusionInlineCommentController', 'services/' => array( 'path/' => array( 'complete/' => 'DiffusionPathCompleteController', 'validate/' => 'DiffusionPathValidateController', ), ), 'symbol/(?P[^/]+)/' => 'DiffusionSymbolController', 'external/' => 'DiffusionExternalController', ), '/daemon/' => array( 'task/(?P\d+)/' => 'PhabricatorWorkerTaskDetailController', 'task/(?P\d+)/(?P[^/]+)/' => 'PhabricatorWorkerTaskUpdateController', 'log/' => array( '' => 'PhabricatorDaemonLogListController', 'combined/' => 'PhabricatorDaemonCombinedLogController', '(?P\d+)/' => 'PhabricatorDaemonLogViewController', ), 'timeline/' => 'PhabricatorDaemonTimelineConsoleController', 'timeline/(?P\d+)/' => 'PhabricatorDaemonTimelineEventController', '' => 'PhabricatorDaemonConsoleController', ), '/herald/' => array( '' => 'HeraldHomeController', 'view/(?P[^/]+)/(?:(?P[^/]+)/)?' => 'HeraldHomeController', 'new/(?:(?P[^/]+)/(?:(?P[^/]+)/)?)?' => 'HeraldNewController', 'rule/(?:(?P\d+)/)?' => 'HeraldRuleController', 'history/(?:(?P\d+)/)?' => 'HeraldRuleEditHistoryController', 'delete/(?P\d+)/' => 'HeraldDeleteController', 'test/' => 'HeraldTestConsoleController', 'transcript/' => 'HeraldTranscriptListController', 'transcript/(?P\d+)/(?:(?P\w+)/)?' => 'HeraldTranscriptController', ), '/uiexample/' => array( '' => 'PhabricatorUIExampleRenderController', 'view/(?P[^/]+)/' => 'PhabricatorUIExampleRenderController', ), '/owners/' => array( '' => 'PhabricatorOwnersListController', 'view/(?P[^/]+)/' => 'PhabricatorOwnersListController', 'edit/(?P\d+)/' => 'PhabricatorOwnersEditController', 'new/' => 'PhabricatorOwnersEditController', 'package/(?P\d+)/' => 'PhabricatorOwnersDetailController', 'delete/(?P\d+)/' => 'PhabricatorOwnersDeleteController', ), '/audit/' => array( '' => 'PhabricatorAuditListController', 'view/(?P[^/]+)/(?:(?P[^/]+)/)?' => 'PhabricatorAuditListController', 'addcomment/' => 'PhabricatorAuditAddCommentController', 'preview/(?P\d+)/' => 'PhabricatorAuditPreviewController', ), '/xhpast/' => array( '' => 'PhabricatorXHPASTViewRunController', 'view/(?P\d+)/' => 'PhabricatorXHPASTViewFrameController', 'frameset/(?P\d+)/' => 'PhabricatorXHPASTViewFramesetController', 'input/(?P\d+)/' => 'PhabricatorXHPASTViewInputController', 'tree/(?P\d+)/' => 'PhabricatorXHPASTViewTreeController', 'stream/(?P\d+)/' => 'PhabricatorXHPASTViewStreamController', ), '/status/' => 'PhabricatorStatusController', '/paste/' => array( '' => 'PhabricatorPasteListController', 'filter/(?P\w+)/' => 'PhabricatorPasteListController', ), '/P(?P\d+)' => 'PhabricatorPasteViewController', '/help/' => array( 'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController', ), '/countdown/' => array( '' => 'PhabricatorCountdownListController', '(?P\d+)/' => 'PhabricatorCountdownViewController', 'edit/(?:(?P\d+)/)?' => 'PhabricatorCountdownEditController', 'delete/(?P\d+)/' => 'PhabricatorCountdownDeleteController' ), '/V(?P\d+)' => 'PhabricatorSlowvotePollController', '/vote/' => array( '(?:view/(?P\w+)/)?' => 'PhabricatorSlowvoteListController', 'create/' => 'PhabricatorSlowvoteCreateController', ), // Match "/w/" with slug "/". '/w(?P/)' => 'PhrictionDocumentController', // Match "/w/x/y/z/" with slug "x/y/z/". '/w/(?P.+/)' => 'PhrictionDocumentController', '/phriction/' => array( '' => 'PhrictionListController', 'list/(?P[^/]+)/' => 'PhrictionListController', 'history(?P/)' => 'PhrictionHistoryController', 'history/(?P.+/)' => 'PhrictionHistoryController', 'edit/(?:(?P\d+)/)?' => 'PhrictionEditController', 'delete/(?P\d+)/' => 'PhrictionDeleteController', 'preview/' => 'PhrictionDocumentPreviewController', 'diff/(?P\d+)/' => 'PhrictionDiffController', ), '/phame/' => array( '' => 'PhamePostListController', 'post/' => array( '' => 'PhamePostListController', 'delete/(?P[^/]+)/' => 'PhamePostDeleteController', 'edit/(?P[^/]+)/' => 'PhamePostEditController', 'new/' => 'PhamePostEditController', 'preview/' => 'PhamePostPreviewController', 'view/(?P[^/]+)/' => 'PhamePostViewController', ), 'draft/' => array( '' => 'PhameDraftListController', 'new/' => 'PhamePostEditController', ), 'posts/' => array( '' => 'PhamePostListController', '(?P\w+)/' => 'PhamePostListController', '(?P\w+)/(?P.+/)' => 'PhamePostViewController', ), ), '/calendar/' => array( '' => 'PhabricatorCalendarBrowseController', ), '/drydock/' => array( '' => 'DrydockResourceListController', 'resource/' => 'DrydockResourceListController', 'resource/allocate/' => 'DrydockResourceAllocateController', 'host/' => array( '' => 'DrydockHostListController', 'edit/' => 'DrydockHostEditController', 'edit/(?P\d+)/' => 'DrydockhostEditController', ), 'lease/' => 'DrydockLeaseListController', 'log/' => 'DrydockLogController', ), '/chatlog/' => array( '' => 'PhabricatorChatLogChannelListController', 'channel/(?P[^/]+)/' => 'PhabricatorChatLogChannelLogController', ), '/aphlict/' => 'PhabricatorAphlictTestPageController', '/flag/' => array( '' => 'PhabricatorFlagListController', 'view/(?P[^/]+)/' => 'PhabricatorFlagListController', 'edit/(?P[^/]+)/' => 'PhabricatorFlagEditController', 'delete/(?P\d+)/' => 'PhabricatorFlagDeleteController', ), '/phortune/' => array( 'stripe/' => array( 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController', ), ), ); } protected function getResourceURIMapRules() { return array( '/res/' => array( '(?Ppkg/)?'. '(?P[a-f0-9]{8})/'. '(?P.+\.(?:css|js|jpg|png|swf|gif))' => '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) { + $request = $this->getRequest(); + + // For Conduit requests, return a Conduit response. + if ($request->isConduit()) { + $response = new ConduitAPIResponse(); + $response->setErrorCode(get_class($ex)); + $response->setErrorInfo($ex->getMessage()); + + return id(new AphrontJSONResponse()) + ->setContent($response->toDictionary()); + } + + // For non-workflow requests, return a Ajax response. + if ($request->isAjax() && !$request->isJavelinWorkflow()) { + $response = new AphrontAjaxResponse(); + $response->setError( + array( + 'code' => get_class($ex), + 'info' => $ex->getMessage(), + )); + return $response; + } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $user = $this->getRequest()->getUser(); + $user = $request->getUser(); if (!$user) { // If we hit an exception very early, we won't have a user. $user = new PhabricatorUser(); } if ($ex instanceof PhabricatorPolicyException) { $content = '
'. phutil_escape_html($ex->getMessage()). '
'; $dialog = new AphrontDialogView(); $dialog ->setTitle( $is_serious ? 'Access Denied' : "You Shall Not Pass") ->setClass('aphront-access-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } else { $dialog->addCancelButton('/', $is_serious ? 'OK' : 'Away With Thee'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof AphrontUsageException) { $error = new AphrontErrorView(); $error->setTitle(phutil_escape_html($ex->getTitle())); $error->appendChild(phutil_escape_html($ex->getMessage())); $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->appendChild($error); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } // 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(), $user); } else { $trace = null; } $content = '
'. '
'.$message.'
'. $trace. '
'; $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( '
'. $response->buildResponseString(). '
'); $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, PhabricatorUser $user) { $libraries = PhutilBootloader::getInstance()->getAllLibraries(); // TODO: Make this configurable? $path = 'https://secure.phabricator.com/diffusion/%s/browse/master/src/'; $callsigns = array( 'arcanist' => 'ARC', 'phutil' => 'PHU', 'phabricator' => 'P', ); $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($callsigns[$lib])) { $attrs = array('title' => $file); try { $attrs['href'] = $user->loadEditorLink( '/src/'.$relative, $part['line'], $callsigns[$lib]); } catch (Exception $ex) { // The database can be inaccessible. } if (empty($attrs['href'])) { $attrs['href'] = sprintf($path, $callsigns[$lib]). str_replace(DIRECTORY_SEPARATOR, '/', $relative). '$'.$part['line']; $attrs['target'] = '_blank'; } $file_name = phutil_render_tag( 'a', $attrs, 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 = '(Internal)'; } $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 '
'. '
Stack Trace
'. $table->render(). '
'; } } diff --git a/src/aphront/default/configuration/__init__.php b/src/aphront/default/configuration/__init__.php index c28e3a05f..83e885d5e 100644 --- a/src/aphront/default/configuration/__init__.php +++ b/src/aphront/default/configuration/__init__.php @@ -1,30 +1,32 @@ host = $host; $this->path = $path; } final public function setApplicationConfiguration( $application_configuration) { $this->applicationConfiguration = $application_configuration; return $this; } final public function getApplicationConfiguration() { return $this->applicationConfiguration; } final public function getPath() { return $this->path; } final public function getHost() { return $this->host; } /* -( Accessing Request Data )--------------------------------------------- */ /** * @task data */ final public function setRequestData(array $request_data) { $this->requestData = $request_data; return $this; } /** * @task data */ final public function getRequestData() { return $this->requestData; } /** * @task data */ final public function getInt($name, $default = null) { if (isset($this->requestData[$name])) { return (int)$this->requestData[$name]; } else { return $default; } } /** * @task data */ final public function getBool($name, $default = null) { if (isset($this->requestData[$name])) { if ($this->requestData[$name] === 'true') { return true; } else if ($this->requestData[$name] === 'false') { return false; } else { return (bool)$this->requestData[$name]; } } else { return $default; } } /** * @task data */ final public function getStr($name, $default = null) { if (isset($this->requestData[$name])) { $str = (string)$this->requestData[$name]; // Normalize newline craziness. $str = str_replace( array("\r\n", "\r"), array("\n", "\n"), $str); return $str; } else { return $default; } } /** * @task data */ final public function getArr($name, $default = array()) { if (isset($this->requestData[$name]) && is_array($this->requestData[$name])) { return $this->requestData[$name]; } else { return $default; } } /** * @task data */ final public function getStrList($name, $default = array()) { if (!isset($this->requestData[$name])) { return $default; } $list = $this->getStr($name); $list = preg_split('/[,\n]/', $list); $list = array_map('trim', $list); $list = array_filter($list, 'strlen'); $list = array_values($list); return $list; } /** * @task data */ final public function getExists($name) { return array_key_exists($name, $this->requestData); } final public function isHTTPPost() { return ($_SERVER['REQUEST_METHOD'] == 'POST'); } final public function isAjax() { return $this->getExists(self::TYPE_AJAX); } + final public function isJavelinWorkflow() { + return $this->getExists(self::TYPE_WORKFLOW); + } + final public function isConduit() { return $this->getExists(self::TYPE_CONDUIT); } public static function getCSRFTokenName() { return '__csrf__'; } public static function getCSRFHeaderName() { return 'X-Phabricator-Csrf'; } final public function validateCSRF() { $token_name = self::getCSRFTokenName(); $token = $this->getStr($token_name); // No token in the request, check the HTTP header which is added for Ajax // requests. if (empty($token)) { // PHP mangles HTTP headers by uppercasing them and replacing hyphens with // underscores, then prepending 'HTTP_'. $php_index = self::getCSRFHeaderName(); $php_index = strtoupper($php_index); $php_index = str_replace('-', '_', $php_index); $php_index = 'HTTP_'.$php_index; $token = idx($_SERVER, $php_index); } $valid = $this->getUser()->validateCSRFToken($token); if (!$valid) { // Add some diagnostic details so we can figure out if some CSRF issues // are JS problems or people accessing Ajax URIs directly with their // browsers. if ($token) { $token_info = "with an invalid CSRF token"; } else { $token_info = "without a CSRF token"; } if ($this->isAjax()) { $more_info = "(This was an Ajax request, {$token_info}.)"; } else { $more_info = "(This was a web request, {$token_info}.)"; } // This should only be able to happen if you load a form, pull your // internet for 6 hours, and then reconnect and immediately submit, // but give the user some indication of what happened since the workflow // is incredibly confusing otherwise. throw new AphrontCSRFException( "The form you just submitted did not include a valid CSRF token. ". "This token is a technical security measure which prevents a ". "certain type of login hijacking attack. However, the token can ". "become invalid if you leave a page open for more than six hours ". "without a connection to the internet. To fix this problem: reload ". "the page, and then resubmit it.\n\n". $more_info); } return true; } final public function isFormPost() { $post = $this->getExists(self::TYPE_FORM) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } final public function getCookie($name, $default = null) { return idx($_COOKIE, $name, $default); } final public function clearCookie($name) { $this->setCookie($name, '', time() - (60 * 60 * 24 * 30)); } final public function setCookie($name, $value, $expire = null) { // Ensure cookies are only set on the configured domain. $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $base_uri = new PhutilURI($base_uri); $base_domain = $base_uri->getDomain(); $base_protocol = $base_uri->getProtocol(); // The "Host" header may include a port number; if so, ignore it. We can't // use PhutilURI since there's no URI scheme. list($actual_host) = explode(':', $this->getHost(), 2); if ($base_domain != $actual_host) { throw new Exception( "This install of Phabricator is configured as '{$base_domain}' but ". "you are accessing it via '{$actual_host}'. Access Phabricator via ". "the primary configured domain."); } if ($expire === null) { $expire = time() + (60 * 60 * 24 * 365 * 5); } $is_secure = ($base_protocol == 'https'); setcookie( $name, $value, $expire, $path = '/', $base_domain, $is_secure, $http_only = true); return $this; } final public function setUser($user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } final public function getRequestURI() { $get = $_GET; unset($get['__path__']); $path = phutil_escape_uri($this->getPath()); return id(new PhutilURI($path))->setQueryParams($get); } final public function isDialogFormPost() { return $this->isFormPost() && $this->getStr('__dialog__'); } final public function getRemoteAddr() { return $_SERVER['REMOTE_ADDR']; } } diff --git a/src/aphront/response/ajax/AphrontAjaxResponse.php b/src/aphront/response/ajax/AphrontAjaxResponse.php index 2195b02aa..1e7bb01f6 100644 --- a/src/aphront/response/ajax/AphrontAjaxResponse.php +++ b/src/aphront/response/ajax/AphrontAjaxResponse.php @@ -1,50 +1,55 @@ content = $content; return $this; } + public function setError($error) { + $this->error = $error; + return $this; + } + public function buildResponseString() { $response = CelerityAPI::getStaticResourceResponse(); $object = $response->buildAjaxResponse( $this->content, $this->error); $response_json = $this->encodeJSONForHTTPResponse($object); return $this->addJSONShield($response_json, $use_javelin_shield = true); } public function getHeaders() { $headers = array( array('Content-Type', 'text/plain; charset=UTF-8'), ); $headers = array_merge(parent::getHeaders(), $headers); return $headers; } } diff --git a/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php b/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php index ca16749bd..4dca060e7 100644 --- a/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php +++ b/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php @@ -1,51 +1,51 @@ getRequest(); $user = $request->getUser(); // NOTE: Throws if valid CSRF token is not present in the request. $request->validateCSRF(); $data = file_get_contents('php://input'); $name = $request->getStr('name'); - $file = PhabricatorFile::newFromFileData( + $file = PhabricatorFile::newFromXHRUpload( $data, array( 'name' => $request->getStr('name'), 'authorPHID' => $user->getPHID(), )); $view = new AphrontAttachedFileView(); $view->setFile($file); return id(new AphrontAjaxResponse())->setContent( array( 'id' => $file->getID(), 'phid' => $file->getPHID(), 'html' => $view->render(), 'uri' => $file->getBestURI(), )); } } diff --git a/src/applications/files/exception/upload/PhabricatorFileUploadException.php b/src/applications/files/exception/upload/PhabricatorFileUploadException.php index d48e2a4e3..e15ae6fb5 100644 --- a/src/applications/files/exception/upload/PhabricatorFileUploadException.php +++ b/src/applications/files/exception/upload/PhabricatorFileUploadException.php @@ -1,43 +1,47 @@ "Uploaded file is too large: file is larger than the ". "'upload_max_size' setting in php.ini.", UPLOAD_ERR_FORM_SIZE => "File is too large.", UPLOAD_ERR_PARTIAL => "File was only partially transferred, upload did not complete.", UPLOAD_ERR_NO_FILE => "No file was uploaded.", UPLOAD_ERR_NO_TMP_DIR => "Unable to write file: temporary directory does not exist.", UPLOAD_ERR_CANT_WRITE => "Unable to write file: failed to write to temporary directory.", UPLOAD_ERR_EXTENSION => "Unable to upload: a PHP extension stopped the upload.", + + -1000 => + "Uploaded file exceeds limit in Phabricator ". + "'storage.upload-size-limit' configuration.", ); $message = idx($map, $code, "Upload failed: unknown error."); parent::__construct($message, $code); } } diff --git a/src/applications/files/storage/file/PhabricatorFile.php b/src/applications/files/storage/file/PhabricatorFile.php index 59121f04f..08b091e05 100644 --- a/src/applications/files/storage/file/PhabricatorFile.php +++ b/src/applications/files/storage/file/PhabricatorFile.php @@ -1,370 +1,388 @@ true, ) + 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 newFromFileData($data, array $params = array()) { + 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); + } + } + + public static function newFromFileData($data, array $params = array()) { $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector'); $engines = $selector->selectStorageEngines($data, $params); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $data_handle = null; $engine_identifier = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($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 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."); } // We stored the file somewhere so stop trying to write it to other // places. break; } catch (Exception $ex) { if ($ex instanceof PhabricatorFileStorageConfigurationException) { // If an engine is outright misconfigured (or misimplemented), raise // that immediately since it probably needs attention. throw $ex; } // If an engine doesn't work, keep trying all the other valid engines // in case something else works. phlog($ex); $exceptions[] = $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); // 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->setContentHash(PhabricatorHash::digest($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)); } $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() { 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 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 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() { $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->getSecretKey()); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } return parent::save(); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } } diff --git a/src/applications/files/storage/file/__init__.php b/src/applications/files/storage/file/__init__.php index e72e0f6ae..efd290a2b 100644 --- a/src/applications/files/storage/file/__init__.php +++ b/src/applications/files/storage/file/__init__.php @@ -1,27 +1,28 @@