diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php index a644c45a9..8e031a977 100644 --- a/src/aphront/response/AphrontProxyResponse.php +++ b/src/aphront/response/AphrontProxyResponse.php @@ -1,67 +1,74 @@ <?php /** * Base class for responses which augment other types of responses. For example, * a response might be substantially an Ajax response, but add structure to the * response content. It can do this by extending @{class:AphrontProxyResponse}, * instantiating an @{class:AphrontAjaxResponse} in @{method:buildProxy}, and - * then using the proxy to construct the response string in - * @{method:buildResponseString}. + * then constructing a real @{class:AphrontAjaxResponse} in + * @{method:reduceProxyResponse}. * * @group aphront */ abstract class AphrontProxyResponse extends AphrontResponse { private $proxy; protected function getProxy() { if (!$this->proxy) { $this->proxy = $this->buildProxy(); } return $this->proxy; } public function setRequest($request) { $this->getProxy()->setRequest($request); return $this; } public function getRequest() { return $this->getProxy()->getRequest(); } public function getHeaders() { return $this->getProxy()->getHeaders(); } public function setCacheDurationInSeconds($duration) { $this->getProxy()->setCacheDurationInSeconds($duration); return $this; } public function setLastModified($epoch_timestamp) { $this->getProxy()->setLastModified($epoch_timestamp); return $this; } public function setHTTPResponseCode($code) { $this->getProxy()->setHTTPResponseCode($code); return $this; } public function getHTTPResponseCode() { return $this->getProxy()->getHTTPResponseCode(); } public function setFrameable($frameable) { $this->getProxy()->setFrameable($frameable); return $this; } public function getCacheHeaders() { return $this->getProxy()->getCacheHeaders(); } abstract protected function buildProxy(); + abstract public function reduceProxyResponse(); + + final public function buildResponseString() { + throw new Exception( + "AphrontProxyResponse must implement reduceProxyResponse()."); + } + } diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 39ca6fdfd..297ea6502 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -1,293 +1,309 @@ <?php abstract class PhabricatorController extends AphrontController { private $handles; public function shouldRequireLogin() { // If this install is configured to allow public resources and the // controller works in public mode, allow the request through. $is_public_allowed = PhabricatorEnv::getEnvConfig('policy.allow-public'); if ($is_public_allowed && $this->shouldAllowPublic()) { return false; } return true; } public function shouldRequireAdmin() { return false; } public function shouldRequireEnabledUser() { return true; } public function shouldAllowPublic() { return false; } public function shouldRequireEmailVerification() { $need_verify = PhabricatorUserEmail::isEmailVerificationRequired(); $need_login = $this->shouldRequireLogin(); return ($need_login && $need_verify); } final public function willBeginExecution() { $request = $this->getRequest(); $user = new PhabricatorUser(); $phusr = $request->getCookie('phusr'); $phsid = $request->getCookie('phsid'); if (strlen($phusr) && $phsid) { $info = queryfx_one( $user->establishConnection('r'), 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID AND s.type LIKE %> AND s.sessionKey = %s', $user->getTableName(), 'phabricator_session', 'web-', $phsid); if ($info) { $user->loadFromArray($info); } } $translation = $user->getTranslation(); if ($translation && $translation != PhabricatorEnv::getEnvConfig('translation.provider')) { $translation = newv($translation, array()); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getTranslations()); } $request->setUser($user); if ($user->getIsDisabled() && $this->shouldRequireEnabledUser()) { $disabled_user_controller = new PhabricatorDisabledUserController( $request); return $this->delegateToController($disabled_user_controller); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST, array( 'request' => $request, 'controller' => get_class($this), )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $checker_controller = $event->getValue('controller'); if ($checker_controller != get_class($this)) { return $this->delegateToController($checker_controller); } if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) { if ($user->getConsoleEnabled() || PhabricatorEnv::getEnvConfig('darkconsole.always-on')) { $console = new DarkConsoleCore(); $request->getApplicationConfiguration()->setConsole($console); } } if ($this->shouldRequireLogin() && !$user->getPHID()) { $login_controller = new PhabricatorLoginController($request); return $this->delegateToController($login_controller); } if ($this->shouldRequireEmailVerification()) { $email = $user->loadPrimaryEmail(); if (!$email) { throw new Exception( "No primary email address associated with this account!"); } if (!$email->getIsVerified()) { $verify_controller = new PhabricatorMustVerifyEmailController($request); return $this->delegateToController($verify_controller); } } if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) { return new Aphront403Response(); } } public function buildStandardPageView() { $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->setController($this); return $view; } public function buildStandardPageResponse($view, array $data) { $page = $this->buildStandardPageView(); $page->appendChild($view); $response = new AphrontWebpageResponse(); $response->setContent($page->render()); return $response; } public function getApplicationURI($path = '') { if (!$this->getCurrentApplication()) { throw new Exception("No application!"); } return $this->getCurrentApplication()->getBaseURI().ltrim($path, '/'); } public function buildApplicationPage($view, array $options) { $page = $this->buildStandardPageView(); $application = $this->getCurrentApplication(); if ($application) { $page->setApplicationName($application->getName()); $page->setTitle(idx($options, 'title')); if ($application->getTitleGlyph()) { $page->setGlyph($application->getTitleGlyph()); } } if (!($view instanceof AphrontSideNavFilterView)) { $nav = new AphrontSideNavFilterView(); $nav->appendChild($view); $view = $nav; } $view->setUser($this->getRequest()->getUser()); $view->setFlexNav(true); $page->appendChild($view); if (idx($options, 'device')) { $page->setDeviceReady(true); $view->appendChild($page->renderFooter()); } $application_menu = $this->buildApplicationMenu(); if ($application_menu) { $page->setApplicationMenu($application_menu); } $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } public function didProcessRequest($response) { $request = $this->getRequest(); $response->setRequest($request); + + $seen = array(); + while ($response instanceof AphrontProxyResponse) { + + $hash = spl_object_hash($response); + if (isset($seen[$hash])) { + $seen[] = get_class($response); + throw new Exception( + "Cycle while reducing proxy responses: ". + implode(' -> ', $seen)); + } + $seen[$hash] = get_class($response); + + $response = $response->reduceProxyResponse(); + } + if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->setController($this); $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; } protected function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Attempting to access handle which wasn't loaded: {$phid}"); } return $this->handles[$phid]; } protected function loadHandles(array $phids) { $phids = array_filter($phids); $this->handles = $this->loadViewerHandles($phids); return $this; } protected function getLoadedHandles() { return $this->handles; } protected function loadViewerHandles(array $phids) { return id(new PhabricatorObjectHandleData($phids)) ->setViewer($this->getRequest()->getUser()) ->loadHandles(); } /** * Render a list of links to handles, identified by PHIDs. The handles must * already be loaded. * * @param list<phid> List of PHIDs to render links to. * @param string Style, one of "\n" (to put each item on its own line) * or "," (to list items inline, separated by commas). * @return string Rendered list of handle links. */ protected function renderHandlesForPHIDs(array $phids, $style = "\n") { $style_map = array( "\n" => '<br />', ',' => ', ', ); if (empty($style_map[$style])) { throw new Exception("Unknown handle list style '{$style}'!"); } $items = array(); foreach ($phids as $phid) { $items[] = $this->getHandle($phid)->renderLink(); } return implode($style_map[$style], $items); } protected function buildApplicationMenu() { return null; } protected function buildApplicationCrumbs() { $crumbs = array(); $application = $this->getCurrentApplication(); if ($application) { $sprite = $application->getIconName(); if (!$sprite) { $sprite = 'application'; } $crumbs[] = id(new PhabricatorCrumbView()) ->setHref($this->getApplicationURI()) ->setIcon($sprite); } $view = new PhabricatorCrumbsView(); foreach ($crumbs as $crumb) { $view->addCrumb($crumb); } return $view; } } diff --git a/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php b/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php index a0ff24345..cc5eede13 100644 --- a/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php +++ b/src/applications/transactions/response/PhabricatorApplicationTransactionResponse.php @@ -1,65 +1,62 @@ <?php final class PhabricatorApplicationTransactionResponse extends AphrontProxyResponse { private $viewer; private $transactions; private $anchorOffset; protected function buildProxy() { return new AphrontAjaxResponse(); } public function setAnchorOffset($anchor_offset) { $this->anchorOffset = $anchor_offset; return $this; } public function getAnchorOffset() { return $this->anchorOffset; } public function setTransactions($transactions) { assert_instances_of($transactions, 'PhabricatorApplicationTransaction'); $this->transactions = $transactions; return $this; } public function getTransactions() { return $this->transactions; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } - public function buildResponseString() { + public function reduceProxyResponse() { $view = id(new PhabricatorApplicationTransactionView()) ->setViewer($this->getViewer()) ->setTransactions($this->getTransactions()); if ($this->getAnchorOffset()) { $view->setAnchorOffset($this->getAnchorOffset()); } $xactions = mpull($view->buildEvents(), 'render', 'getTransactionPHID'); $content = array( 'xactions' => $xactions, 'spacer' => PhabricatorTimelineView::renderSpacer(), ); - return $this - ->getProxy() - ->setContent($content) - ->buildResponseString(); + return $this->getProxy()->setContent($content); } } diff --git a/src/infrastructure/diff/PhabricatorChangesetResponse.php b/src/infrastructure/diff/PhabricatorChangesetResponse.php index 86f4f3dcc..b4897eb18 100644 --- a/src/infrastructure/diff/PhabricatorChangesetResponse.php +++ b/src/infrastructure/diff/PhabricatorChangesetResponse.php @@ -1,34 +1,34 @@ <?php final class PhabricatorChangesetResponse extends AphrontProxyResponse { private $renderedChangeset; private $coverage; public function setRenderedChangeset($rendered_changeset) { $this->renderedChangeset = $rendered_changeset; return $this; } public function setCoverage($coverage) { $this->coverage = $coverage; return $this; } protected function buildProxy() { return new AphrontAjaxResponse(); } - public function buildResponseString() { + public function reduceProxyResponse() { $content = array( 'changeset' => $this->renderedChangeset, ); if ($this->coverage) { $content['coverage'] = $this->coverage; } - return $this->getProxy()->setContent($content)->buildResponseString(); + return $this->getProxy()->setContent($content); } }