diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 10a4dd79ef..fa3e1ee107 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -1,586 +1,591 @@ shouldRequireLogin()) { return false; } if (!$this->shouldRequireEnabledUser()) { return false; } if ($this->shouldAllowPartialSessions()) { return false; } $user = $this->getRequest()->getUser(); if (!$user->getIsStandardUser()) { return false; } return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth'); } public function shouldAllowLegallyNonCompliantUsers() { return false; } public function willBeginExecution() { $request = $this->getRequest(); if ($request->getUser()) { // NOTE: Unit tests can set a user explicitly. Normal requests are not // permitted to do this. PhabricatorTestCase::assertExecutingUnitTests(); $user = $request->getUser(); } else { $user = new PhabricatorUser(); $session_engine = new PhabricatorAuthSessionEngine(); $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); if (strlen($phsid)) { $session_user = $session_engine->loadUserForSession( PhabricatorAuthSession::TYPE_WEB, $phsid); if ($session_user) { $user = $session_user; } } else { // If the client doesn't have a session token, generate an anonymous // session. This is used to provide CSRF protection to logged-out users. $phsid = $session_engine->establishSession( PhabricatorAuthSession::TYPE_WEB, null, $partial = false); // This may be a resource request, in which case we just don't set // the cookie. if ($request->canSetCookies()) { $request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid); } } if (!$user->isLoggedIn()) { $user->attachAlternateCSRFString(PhabricatorHash::digest($phsid)); } $request->setUser($user); } $locale_code = $user->getTranslation(); if ($locale_code) { PhabricatorEnv::setLocaleCode($locale_code); } $preferences = $user->loadPreferences(); if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) { $dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE; if ($preferences->getPreference($dark_console) || PhabricatorEnv::getEnvConfig('darkconsole.always-on')) { $console = new DarkConsoleCore(); $request->getApplicationConfiguration()->setConsole($console); } } // NOTE: We want to set up the user first so we can render a real page // here, but fire this before any real logic. $restricted = array( 'code', ); foreach ($restricted as $parameter) { if ($request->getExists($parameter)) { if (!$this->shouldAllowRestrictedParameter($parameter)) { throw new Exception( pht( 'Request includes restricted parameter "%s", but this '. 'controller ("%s") does not whitelist it. Refusing to '. 'serve this request because it might be part of a redirection '. 'attack.', $parameter, get_class($this))); } } } if ($this->shouldRequireEnabledUser()) { if ($user->isLoggedIn() && !$user->getIsApproved()) { $controller = new PhabricatorAuthNeedsApprovalController(); return $this->delegateToController($controller); } if ($user->getIsDisabled()) { $controller = new PhabricatorDisabledUserController(); return $this->delegateToController($controller); } } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST, array( 'request' => $request, 'controller' => $this, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $checker_controller = $event->getValue('controller'); if ($checker_controller != $this) { return $this->delegateToController($checker_controller); } $auth_class = 'PhabricatorAuthApplication'; $auth_application = PhabricatorApplication::getByClass($auth_class); // Require partial sessions to finish login before doing anything. if (!$this->shouldAllowPartialSessions()) { if ($user->hasSession() && $user->getSession()->getIsPartial()) { $login_controller = new PhabricatorAuthFinishController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($login_controller); } } // Check if the user needs to configure MFA. $need_mfa = $this->shouldRequireMultiFactorEnrollment(); $have_mfa = $user->getIsEnrolledInMultiFactor(); if ($need_mfa && !$have_mfa) { // Check if the cache is just out of date. Otherwise, roadblock the user // and require MFA enrollment. $user->updateMultiFactorEnrollment(); if (!$user->getIsEnrolledInMultiFactor()) { $mfa_controller = new PhabricatorAuthNeedsMultiFactorController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($mfa_controller); } } if ($this->shouldRequireLogin()) { // This actually means we need either: // - a valid user, or a public controller; and // - permission to see the application. $allow_public = $this->shouldAllowPublic() && PhabricatorEnv::getEnvConfig('policy.allow-public'); // If this controller isn't public, and the user isn't logged in, require // login. if (!$allow_public && !$user->isLoggedIn()) { $login_controller = new PhabricatorAuthStartController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($login_controller); } if ($user->isLoggedIn()) { if ($this->shouldRequireEmailVerification()) { if (!$user->getIsEmailVerified()) { $controller = new PhabricatorMustVerifyEmailController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($controller); } } } // If the user doesn't have access to the application, don't let them use // any of its controllers. We query the application in order to generate // a policy exception if the viewer doesn't have permission. $application = $this->getCurrentApplication(); if ($application) { id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withPHIDs(array($application->getPHID())) ->executeOne(); } } if (!$this->shouldAllowLegallyNonCompliantUsers()) { $legalpad_class = 'PhabricatorLegalpadApplication'; $legalpad = id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withClasses(array($legalpad_class)) ->withInstalled(true) ->execute(); $legalpad = head($legalpad); $doc_query = id(new LegalpadDocumentQuery()) ->setViewer($user) ->withSignatureRequired(1) ->needViewerSignatures(true); if ($user->hasSession() && !$user->getSession()->getIsPartial() && !$user->getSession()->getSignedLegalpadDocuments() && $user->isLoggedIn() && $legalpad) { $sign_docs = $doc_query->execute(); $must_sign_docs = array(); foreach ($sign_docs as $sign_doc) { if (!$sign_doc->getUserSignature($user->getPHID())) { $must_sign_docs[] = $sign_doc; } } if ($must_sign_docs) { $controller = new LegalpadDocumentSignController(); $this->getRequest()->setURIMap(array( 'id' => head($must_sign_docs)->getID(), )); $this->setCurrentApplication($legalpad); return $this->delegateToController($controller); } else { $engine = id(new PhabricatorAuthSessionEngine()) ->signLegalpadDocuments($user, $sign_docs); } } } // NOTE: We do this last so that users get a login page instead of a 403 // if they need to login. 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); return $this->buildPageResponse($page); } private function buildPageResponse($page) { if ($this->getRequest()->isQuicksand()) { $response = id(new AphrontAjaxResponse()) ->setContent($page->renderForQuicksand()); } else { $response = id(new AphrontWebpageResponse()) ->setContent($page->render()); } return $response; } public function getApplicationURI($path = '') { if (!$this->getCurrentApplication()) { throw new Exception('No application!'); } return $this->getCurrentApplication()->getApplicationURI($path); } public function buildApplicationPage($view, array $options) { $page = $this->buildStandardPageView(); $title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ? 'Phabricator' : pht('Bacon Ice Cream for Breakfast'); $application = $this->getCurrentApplication(); $page->setTitle(idx($options, 'title', $title)); if ($application) { $page->setApplicationName($application->getName()); if ($application->getTitleGlyph()) { $page->setGlyph($application->getTitleGlyph()); } } if (!($view instanceof AphrontSideNavFilterView)) { $nav = new AphrontSideNavFilterView(); $nav->appendChild($view); $view = $nav; } $user = $this->getRequest()->getUser(); $view->setUser($user); $page->appendChild($view); $object_phids = idx($options, 'pageObjects', array()); if ($object_phids) { $page->appendPageObjects($object_phids); foreach ($object_phids as $object_phid) { PhabricatorFeedStoryNotification::updateObjectNotificationViews( $user, $object_phid); } } if (idx($options, 'device', true)) { $page->setDeviceReady(true); } $page->setShowFooter(idx($options, 'showFooter', true)); $page->setShowChrome(idx($options, 'chrome', true)); $application_menu = $this->buildApplicationMenu(); if ($application_menu) { $page->setApplicationMenu($application_menu); } return $this->buildPageResponse($page); } public function didProcessRequest($response) { // If a bare DialogView is returned, wrap it in a DialogResponse. if ($response instanceof AphrontDialogView) { $response = id(new AphrontDialogResponse())->setDialog($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() && !$request->isQuicksand()) { $dialog = $response->getDialog(); $title = $dialog->getTitle(); $short = $dialog->getShortTitle(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(coalesce($short, $title)); $page_content = array( $crumbs, $response->buildResponseString(), ); $view = id(new PhabricatorStandardPageView()) ->setRequest($request) ->setController($this) ->setDeviceReady(true) ->setTitle($title) ->appendChild($page_content); $response = id(new AphrontWebpageResponse()) ->setContent($view->render()) ->setHTTPResponseCode($response->getHTTPResponseCode()); } else { $response->getDialog()->setIsStandalone(true); return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax() || $request->isQuicksand()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } return $response; } + /** + * WARNING: Do not call this in new code. + * + * @deprecated See "Handles Technical Documentation". + */ protected function loadViewerHandles(array $phids) { return id(new PhabricatorHandleQuery()) ->setViewer($this->getRequest()->getUser()) ->withPHIDs($phids) ->execute(); } public function buildApplicationMenu() { return null; } protected function buildApplicationCrumbs() { $crumbs = array(); $application = $this->getCurrentApplication(); if ($application) { $icon = $application->getFontIcon(); if (!$icon) { $icon = 'fa-puzzle'; } $crumbs[] = id(new PHUICrumbView()) ->setHref($this->getApplicationURI()) ->setName($application->getName()) ->setIcon($icon); } $view = new PHUICrumbsView(); foreach ($crumbs as $crumb) { $view->addCrumb($crumb); } return $view; } protected function hasApplicationCapability($capability) { return PhabricatorPolicyFilter::hasCapability( $this->getRequest()->getUser(), $this->getCurrentApplication(), $capability); } protected function requireApplicationCapability($capability) { PhabricatorPolicyFilter::requireCapability( $this->getRequest()->getUser(), $this->getCurrentApplication(), $capability); } protected function explainApplicationCapability( $capability, $positive_message, $negative_message) { $can_act = $this->hasApplicationCapability($capability); if ($can_act) { $message = $positive_message; $icon_name = 'fa-play-circle-o lightgreytext'; } else { $message = $negative_message; $icon_name = 'fa-lock'; } $icon = id(new PHUIIconView()) ->setIconFont($icon_name); require_celerity_resource('policy-css'); $phid = $this->getCurrentApplication()->getPHID(); $explain_uri = "/policy/explain/{$phid}/{$capability}/"; $message = phutil_tag( 'div', array( 'class' => 'policy-capability-explanation', ), array( $icon, javelin_tag( 'a', array( 'href' => $explain_uri, 'sigil' => 'workflow', ), $message), )); return array($can_act, $message); } public function getDefaultResourceSource() { return 'phabricator'; } /** * Create a new @{class:AphrontDialogView} with defaults filled in. * * @return AphrontDialogView New dialog. */ public function newDialog() { $submit_uri = new PhutilURI($this->getRequest()->getRequestURI()); $submit_uri = $submit_uri->getPath(); return id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setSubmitURI($submit_uri); } protected function buildTransactionTimeline( PhabricatorApplicationTransactionInterface $object, PhabricatorApplicationTransactionQuery $query, PhabricatorMarkupEngine $engine = null, $render_data = array()) { $viewer = $this->getRequest()->getUser(); $xaction = $object->getApplicationTransactionTemplate(); $view = $xaction->getApplicationTransactionViewObject(); $pager = id(new AphrontCursorPagerView()) ->readFromRequest($this->getRequest()) ->setURI(new PhutilURI( '/transactions/showolder/'.$object->getPHID().'/')); $xactions = $query ->setViewer($viewer) ->withObjectPHIDs(array($object->getPHID())) ->needComments(true) ->setReversePaging(false) ->executeWithCursorPager($pager); $xactions = array_reverse($xactions); if ($engine) { foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $view->setMarkupEngine($engine); } $timeline = $view ->setUser($viewer) ->setObjectPHID($object->getPHID()) ->setTransactions($xactions) ->setPager($pager) ->setRenderData($render_data) ->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID')) ->setQuoteRef($this->getRequest()->getStr('quoteRef')); $object->willRenderTimeline($timeline, $this->getRequest()); return $timeline; } } diff --git a/src/docs/tech/handles.diviner b/src/docs/tech/handles.diviner new file mode 100644 index 0000000000..18e33dc133 --- /dev/null +++ b/src/docs/tech/handles.diviner @@ -0,0 +1,101 @@ +@title Handles Technical Documentation +@group handles + +Technical overview of Handles. + +Overview +======== + +Most objects in Phabricator have PHIDs, which are globally unique identifiers +that look like `PHID-USER-2zw4hwdt4i5b5ypikv6x`. If you know the PHID for an +object, you can load a **handle** for that object to get more information +about it. + +Handles are lightweight reference objects which provide some basic information +common across all objects (like their type, icons, names, monograms, URIs, and +whether they are open or closed). Applications don't need to know anything about +other types of objects in order to load and use handles. There are uniform +mechanisms available to load and work with handles which work across all types +of objects in every application. + + +Loading Handles +=============== + +To load handles, you'll usually call `loadHandles(...)` on the viewer: + + $handles = $viewer->loadHandles($phids); + +This returns a @{class:PhabricatorHandleList}. This object behaves like an +array, and you can access handle objects by using their PHIDs as indexes: + + $handle = $handles[$phid]; + +Handles will always load, even if the PHID is invalid or the object it +identifies is restricted or broken. In these cases, the handle will accurately +represent the state of the associated object. This means that you generally do +not need to check if a handle loaded. + + +Rendering Handles +================= + +After loading handles, you'll usually call `renderHandle($phid)` to render a +link to an object: + + $view = $handles->renderHandle($phid); + +This returns a @{class:PHUIHandleView}. The class exposes some methods which +can adjust how the handle renders. + +If you want to render a list of handles, you can use `renderList()`: + + $list_view = $handles->renderList(); + +This returns a @{class:PHUIHandleListView}. This class also exposes some +methods to adjust how the list renders. + +Convenience methods for these operations are also available on the viewer +object itself: + + $view = $viewer->renderHandle($phid); + $list_view = $viewer->renderHandleList($phids); + +When you only need to render a handle once, these methods make it easier. + + +Fetch Semantics +=============== + +When you load and render handles through the viewer, the actual data fetching +occurs just-in-time. Specifically, all of the required PHIDs are queued up +until a concrete representation //needs// to be produced. Handles are then bulk +loaded. + +This means that, unlike most other types of data fetching, it's OK to +single-fetch handles, because they won't //really// single-fetch. This code is +correct and desirable: + + $list->addProperty(pht('Pilot'), $viewer->renderHandle($pilot_phid)); + $list->addProperty(pht('Copilot'), $viewer->renderHandle($copilot_phid)); + +If you're rendering a very large number of handles (for example, 100+ handles +in a result list view) it's //slightly// more efficient to render them through +a @{class:PhabricatorHandleList}: + + $handles = $viewer->loadHandles($phids); + foreach ($items as $item) { + // ... + $view = $handles->renderHandle($item->getPHID()); + // ... + } + +This shaves off a tiny bit of internal bookkeeping overhead. This does not +change the underlying semantics of the data fetch. + +Handles are particularly well suited to use this just-in-time fetch pattern +because they're ubiquitous and code essentially never makes decisions based on +handles, so it's very rare that they need to be made concrete until final page +rendering. Most other kinds of data do not have the same sort of +application-level semantics. This generally makes other objects much less +suitable to be fetched just-in-time.