3 abstract class PhabricatorController
extends AphrontController
{
7 public function shouldRequireLogin() {
11 public function shouldRequireAdmin() {
15 public function shouldRequireEnabledUser() {
19 public function shouldAllowPublic() {
23 public function shouldAllowPartialSessions() {
27 public function shouldRequireEmailVerification() {
28 return PhabricatorUserEmail
::isEmailVerificationRequired();
31 public function shouldAllowRestrictedParameter($parameter_name) {
35 public function shouldRequireMultiFactorEnrollment() {
36 if (!$this->shouldRequireLogin()) {
40 if (!$this->shouldRequireEnabledUser()) {
44 if ($this->shouldAllowPartialSessions()) {
48 $user = $this->getRequest()->getUser();
49 if (!$user->getIsStandardUser()) {
53 return PhabricatorEnv
::getEnvConfig('security.require-multi-factor-auth');
56 public function shouldAllowLegallyNonCompliantUsers() {
60 public function isGlobalDragAndDropUploadEnabled() {
64 public function willBeginExecution() {
65 $request = $this->getRequest();
67 if ($request->getUser()) {
68 // NOTE: Unit tests can set a user explicitly. Normal requests are not
69 // permitted to do this.
70 PhabricatorTestCase
::assertExecutingUnitTests();
71 $user = $request->getUser();
73 $user = new PhabricatorUser();
74 $session_engine = new PhabricatorAuthSessionEngine();
76 $phsid = $request->getCookie(PhabricatorCookies
::COOKIE_SESSION
);
78 $session_user = $session_engine->loadUserForSession(
79 PhabricatorAuthSession
::TYPE_WEB
,
82 $user = $session_user;
85 // If the client doesn't have a session token, generate an anonymous
86 // session. This is used to provide CSRF protection to logged-out users.
87 $phsid = $session_engine->establishSession(
88 PhabricatorAuthSession
::TYPE_WEB
,
92 // This may be a resource request, in which case we just don't set
94 if ($request->canSetCookies()) {
95 $request->setCookie(PhabricatorCookies
::COOKIE_SESSION
, $phsid);
100 if (!$user->isLoggedIn()) {
101 $csrf = PhabricatorHash
::digestWithNamedKey($phsid, 'csrf.alternate');
102 $user->attachAlternateCSRFString($csrf);
105 $request->setUser($user);
108 id(new PhabricatorAuthSessionEngine())
109 ->willServeRequestForUser($user);
111 if (PhabricatorEnv
::getEnvConfig('darkconsole.enabled')) {
112 $dark_console = PhabricatorDarkConsoleSetting
::SETTINGKEY
;
113 if ($user->getUserSetting($dark_console) ||
114 PhabricatorEnv
::getEnvConfig('darkconsole.always-on')) {
115 $console = new DarkConsoleCore();
116 $request->getApplicationConfiguration()->setConsole($console);
120 // NOTE: We want to set up the user first so we can render a real page
121 // here, but fire this before any real logic.
125 foreach ($restricted as $parameter) {
126 if ($request->getExists($parameter)) {
127 if (!$this->shouldAllowRestrictedParameter($parameter)) {
130 'Request includes restricted parameter "%s", but this '.
131 'controller ("%s") does not whitelist it. Refusing to '.
132 'serve this request because it might be part of a redirection '.
140 if ($this->shouldRequireEnabledUser()) {
141 if ($user->getIsDisabled()) {
142 $controller = new PhabricatorDisabledUserController();
143 return $this->delegateToController($controller);
147 $auth_class = 'PhabricatorAuthApplication';
148 $auth_application = PhabricatorApplication
::getByClass($auth_class);
150 // Require partial sessions to finish login before doing anything.
151 if (!$this->shouldAllowPartialSessions()) {
152 if ($user->hasSession() &&
153 $user->getSession()->getIsPartial()) {
154 $login_controller = new PhabricatorAuthFinishController();
155 $this->setCurrentApplication($auth_application);
156 return $this->delegateToController($login_controller);
160 // Require users sign Legalpad documents before we check if they have
161 // MFA. If we don't do this, they can get stuck in a state where they
162 // can't add MFA until they sign, and can't sign until they add MFA.
163 // See T13024 and PHI223.
164 $result = $this->requireLegalpadSignatures();
165 if ($result !== null) {
169 // Check if the user needs to configure MFA.
170 $need_mfa = $this->shouldRequireMultiFactorEnrollment();
171 $have_mfa = $user->getIsEnrolledInMultiFactor();
172 if ($need_mfa && !$have_mfa) {
173 // Check if the cache is just out of date. Otherwise, roadblock the user
174 // and require MFA enrollment.
175 $user->updateMultiFactorEnrollment();
176 if (!$user->getIsEnrolledInMultiFactor()) {
177 $mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
178 $this->setCurrentApplication($auth_application);
179 return $this->delegateToController($mfa_controller);
183 if ($this->shouldRequireLogin()) {
184 // This actually means we need either:
185 // - a valid user, or a public controller; and
186 // - permission to see the application; and
187 // - permission to see at least one Space if spaces are configured.
189 $allow_public = $this->shouldAllowPublic() &&
190 PhabricatorEnv
::getEnvConfig('policy.allow-public');
192 // If this controller isn't public, and the user isn't logged in, require
194 if (!$allow_public && !$user->isLoggedIn()) {
195 $login_controller = new PhabricatorAuthStartController();
196 $this->setCurrentApplication($auth_application);
197 return $this->delegateToController($login_controller);
200 if ($user->isLoggedIn()) {
201 if ($this->shouldRequireEmailVerification()) {
202 if (!$user->getIsEmailVerified()) {
203 $controller = new PhabricatorMustVerifyEmailController();
204 $this->setCurrentApplication($auth_application);
205 return $this->delegateToController($controller);
210 // If Spaces are configured, require that the user have access to at
211 // least one. If we don't do this, they'll get confusing error messages
213 $spaces = PhabricatorSpacesNamespaceQuery
::getSpacesExist();
215 $viewer_spaces = PhabricatorSpacesNamespaceQuery
::getViewerSpaces(
217 if (!$viewer_spaces) {
218 $controller = new PhabricatorSpacesNoAccessController();
219 return $this->delegateToController($controller);
223 // If the user doesn't have access to the application, don't let them use
224 // any of its controllers. We query the application in order to generate
225 // a policy exception if the viewer doesn't have permission.
226 $application = $this->getCurrentApplication();
228 id(new PhabricatorApplicationQuery())
230 ->withPHIDs(array($application->getPHID()))
234 // If users need approval, require they wait here. We do this near the
235 // end so they can take other actions (like verifying email, signing
236 // documents, and enrolling in MFA) while waiting for an admin to take a
237 // look at things. See T13024 for more discussion.
238 if ($this->shouldRequireEnabledUser()) {
239 if ($user->isLoggedIn() && !$user->getIsApproved()) {
240 $controller = new PhabricatorAuthNeedsApprovalController();
241 return $this->delegateToController($controller);
246 // NOTE: We do this last so that users get a login page instead of a 403
247 // if they need to login.
248 if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
249 return new Aphront403Response();
253 public function getApplicationURI($path = '') {
254 if (!$this->getCurrentApplication()) {
255 throw new Exception(pht('No application!'));
257 return $this->getCurrentApplication()->getApplicationURI($path);
260 public function willSendResponse(AphrontResponse
$response) {
261 $request = $this->getRequest();
263 if ($response instanceof AphrontDialogResponse
) {
264 if (!$request->isAjax() && !$request->isQuicksand()) {
265 $dialog = $response->getDialog();
267 $title = $dialog->getTitle();
268 $short = $dialog->getShortTitle();
270 $crumbs = $this->buildApplicationCrumbs();
271 $crumbs->addTextCrumb(coalesce($short, $title));
273 $page_content = array(
275 $response->buildResponseString(),
278 $view = id(new PhabricatorStandardPageView())
279 ->setRequest($request)
280 ->setController($this)
281 ->setDeviceReady(true)
283 ->appendChild($page_content);
285 $response = id(new AphrontWebpageResponse())
286 ->setContent($view->render())
287 ->setHTTPResponseCode($response->getHTTPResponseCode());
289 $response->getDialog()->setIsStandalone(true);
291 return id(new AphrontAjaxResponse())
293 'dialog' => $response->buildResponseString(),
296 } else if ($response instanceof AphrontRedirectResponse
) {
297 if ($request->isAjax() ||
$request->isQuicksand()) {
298 return id(new AphrontAjaxResponse())
301 'redirect' => $response->getURI(),
302 'close' => $response->getCloseDialogBeforeRedirect(),
311 * WARNING: Do not call this in new code.
313 * @deprecated See "Handles Technical Documentation".
315 protected function loadViewerHandles(array $phids) {
316 return id(new PhabricatorHandleQuery())
317 ->setViewer($this->getRequest()->getUser())
322 public function buildApplicationMenu() {
326 protected function buildApplicationCrumbs() {
329 $application = $this->getCurrentApplication();
331 $icon = $application->getIcon();
336 $crumbs[] = id(new PHUICrumbView())
337 ->setHref($this->getApplicationURI())
338 ->setName($application->getName())
342 $view = new PHUICrumbsView();
343 foreach ($crumbs as $crumb) {
344 $view->addCrumb($crumb);
350 protected function hasApplicationCapability($capability) {
351 return PhabricatorPolicyFilter
::hasCapability(
352 $this->getRequest()->getUser(),
353 $this->getCurrentApplication(),
357 protected function requireApplicationCapability($capability) {
358 PhabricatorPolicyFilter
::requireCapability(
359 $this->getRequest()->getUser(),
360 $this->getCurrentApplication(),
364 protected function explainApplicationCapability(
369 $can_act = $this->hasApplicationCapability($capability);
371 $message = $positive_message;
372 $icon_name = 'fa-play-circle-o lightgreytext';
374 $message = $negative_message;
375 $icon_name = 'fa-lock';
378 $icon = id(new PHUIIconView())
379 ->setIcon($icon_name);
381 require_celerity_resource('policy-css');
383 $phid = $this->getCurrentApplication()->getPHID();
384 $explain_uri = "/policy/explain/{$phid}/{$capability}/";
386 $message = phutil_tag(
389 'class' => 'policy-capability-explanation',
396 'href' => $explain_uri,
397 'sigil' => 'workflow',
402 return array($can_act, $message);
405 public function getDefaultResourceSource() {
406 return 'phabricator';
410 * Create a new @{class:AphrontDialogView} with defaults filled in.
412 * @return AphrontDialogView New dialog.
414 public function newDialog() {
415 $submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
416 $submit_uri = $submit_uri->getPath();
418 return id(new AphrontDialogView())
419 ->setUser($this->getRequest()->getUser())
420 ->setSubmitURI($submit_uri);
423 public function newPage() {
424 $page = id(new PhabricatorStandardPageView())
425 ->setRequest($this->getRequest())
426 ->setController($this)
427 ->setDeviceReady(true);
429 $application = $this->getCurrentApplication();
431 $page->setApplicationName($application->getName());
432 if ($application->getTitleGlyph()) {
433 $page->setGlyph($application->getTitleGlyph());
437 $viewer = $this->getRequest()->getUser();
439 $page->setUser($viewer);
445 public function newApplicationMenu() {
446 return id(new PHUIApplicationMenuView())
447 ->setViewer($this->getViewer());
450 public function newCurtainView($object = null) {
451 $viewer = $this->getViewer();
453 $action_id = celerity_generate_unique_node_id();
455 $action_list = id(new PhabricatorActionListView())
459 // NOTE: Applications (objects of class PhabricatorApplication) can't
460 // currently be set here, although they don't need any of the extensions
461 // anyway. This should probably work differently than it does, though.
463 if ($object instanceof PhabricatorLiskDAO
) {
464 $action_list->setObject($object);
468 $curtain = id(new PHUICurtainView())
470 ->setActionList($action_list);
473 $panels = PHUICurtainExtension
::buildExtensionPanels($viewer, $object);
474 foreach ($panels as $panel) {
475 $curtain->addPanel($panel);
482 protected function buildTransactionTimeline(
483 PhabricatorApplicationTransactionInterface
$object,
484 PhabricatorApplicationTransactionQuery
$query = null,
485 PhabricatorMarkupEngine
$engine = null,
486 $view_data = array()) {
488 $request = $this->getRequest();
489 $viewer = $this->getViewer();
490 $xaction = $object->getApplicationTransactionTemplate();
493 $query = PhabricatorApplicationTransactionQuery
::newQueryForObject(
498 'Unable to find transaction query for object of class "%s".',
499 get_class($object)));
503 $pager = id(new AphrontCursorPagerView())
504 ->readFromRequest($request)
505 ->setURI(new PhutilURI(
506 '/transactions/showolder/'.$object->getPHID().'/'));
510 ->withObjectPHIDs(array($object->getPHID()))
512 ->executeWithCursorPager($pager);
513 $xactions = array_reverse($xactions);
515 $timeline_engine = PhabricatorTimelineEngine
::newForObject($object)
517 ->setTransactions($xactions)
518 ->setViewData($view_data);
520 $view = $timeline_engine->buildTimelineView();
523 foreach ($xactions as $xaction) {
524 if ($xaction->getComment()) {
526 $xaction->getComment(),
527 PhabricatorApplicationTransactionComment
::MARKUP_FIELD_COMMENT
);
531 $view->setMarkupEngine($engine);
536 ->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
537 ->setQuoteRef($this->getRequest()->getStr('quoteRef'));
543 public function buildApplicationCrumbsForEditEngine() {
544 // TODO: This is kind of gross, I'm basically just making this public so
545 // I can use it in EditEngine. We could do this without making it public
546 // by using controller delegation, or make it properly public.
547 return $this->buildApplicationCrumbs();
550 private function requireLegalpadSignatures() {
551 if (!$this->shouldRequireLogin()) {
555 if ($this->shouldAllowLegallyNonCompliantUsers()) {
559 $viewer = $this->getViewer();
561 if (!$viewer->hasSession()) {
565 $session = $viewer->getSession();
566 if ($session->getIsPartial()) {
567 // If the user hasn't made it through MFA yet, require they survive
572 if ($session->getSignedLegalpadDocuments()) {
576 if (!$viewer->isLoggedIn()) {
580 $must_sign_docs = array();
581 $sign_docs = array();
583 $legalpad_class = 'PhabricatorLegalpadApplication';
584 $legalpad_installed = PhabricatorApplication
::isClassInstalledForViewer(
587 if ($legalpad_installed) {
588 $sign_docs = id(new LegalpadDocumentQuery())
590 ->withSignatureRequired(1)
591 ->needViewerSignatures(true)
595 foreach ($sign_docs as $sign_doc) {
596 if (!$sign_doc->getUserSignature($viewer->getPHID())) {
597 $must_sign_docs[] = $sign_doc;
602 if (!$must_sign_docs) {
603 // If nothing needs to be signed (either because there are no documents
604 // which require a signature, or because the user has already signed
605 // all of them) mark the session as good and continue.
606 $engine = id(new PhabricatorAuthSessionEngine())
607 ->signLegalpadDocuments($viewer, $sign_docs);
612 $request = $this->getRequest();
615 'id' => head($must_sign_docs)->getID(),
618 $application = PhabricatorApplication
::getByClass($legalpad_class);
619 $this->setCurrentApplication($application);
621 $controller = new LegalpadDocumentSignController();
622 $controller->setIsSessionGate(true);
623 return $this->delegateToController($controller);
627 /* -( Deprecated )--------------------------------------------------------- */
631 * DEPRECATED. Use @{method:newPage}.
633 public function buildStandardPageView() {
634 return $this->newPage();
639 * DEPRECATED. Use @{method:newPage}.
641 public function buildStandardPageResponse($view, array $data) {
642 $page = $this->buildStandardPageView();
643 $page->appendChild($view);
644 return $page->produceAphrontResponse();