Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / base / controller / PhabricatorController.php
bloba463c741d193e07a3cc59e854ffbc56512b2d322
1 <?php
3 abstract class PhabricatorController extends AphrontController {
5 private $handles;
7 public function shouldRequireLogin() {
8 return true;
11 public function shouldRequireAdmin() {
12 return false;
15 public function shouldRequireEnabledUser() {
16 return true;
19 public function shouldAllowPublic() {
20 return false;
23 public function shouldAllowPartialSessions() {
24 return false;
27 public function shouldRequireEmailVerification() {
28 return PhabricatorUserEmail::isEmailVerificationRequired();
31 public function shouldAllowRestrictedParameter($parameter_name) {
32 return false;
35 public function shouldRequireMultiFactorEnrollment() {
36 if (!$this->shouldRequireLogin()) {
37 return false;
40 if (!$this->shouldRequireEnabledUser()) {
41 return false;
44 if ($this->shouldAllowPartialSessions()) {
45 return false;
48 $user = $this->getRequest()->getUser();
49 if (!$user->getIsStandardUser()) {
50 return false;
53 return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
56 public function shouldAllowLegallyNonCompliantUsers() {
57 return false;
60 public function isGlobalDragAndDropUploadEnabled() {
61 return false;
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();
72 } else {
73 $user = new PhabricatorUser();
74 $session_engine = new PhabricatorAuthSessionEngine();
76 $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
77 if (strlen($phsid)) {
78 $session_user = $session_engine->loadUserForSession(
79 PhabricatorAuthSession::TYPE_WEB,
80 $phsid);
81 if ($session_user) {
82 $user = $session_user;
84 } else {
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,
89 null,
90 $partial = false);
92 // This may be a resource request, in which case we just don't set
93 // the cookie.
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.
122 $restricted = array(
123 'code',
125 foreach ($restricted as $parameter) {
126 if ($request->getExists($parameter)) {
127 if (!$this->shouldAllowRestrictedParameter($parameter)) {
128 throw new Exception(
129 pht(
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 '.
133 'attack.',
134 $parameter,
135 get_class($this)));
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) {
166 return $result;
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
193 // login.
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
212 // later on.
213 $spaces = PhabricatorSpacesNamespaceQuery::getSpacesExist();
214 if ($spaces) {
215 $viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
216 $user);
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();
227 if ($application) {
228 id(new PhabricatorApplicationQuery())
229 ->setViewer($user)
230 ->withPHIDs(array($application->getPHID()))
231 ->executeOne();
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(
274 $crumbs,
275 $response->buildResponseString(),
278 $view = id(new PhabricatorStandardPageView())
279 ->setRequest($request)
280 ->setController($this)
281 ->setDeviceReady(true)
282 ->setTitle($title)
283 ->appendChild($page_content);
285 $response = id(new AphrontWebpageResponse())
286 ->setContent($view->render())
287 ->setHTTPResponseCode($response->getHTTPResponseCode());
288 } else {
289 $response->getDialog()->setIsStandalone(true);
291 return id(new AphrontAjaxResponse())
292 ->setContent(array(
293 'dialog' => $response->buildResponseString(),
296 } else if ($response instanceof AphrontRedirectResponse) {
297 if ($request->isAjax() || $request->isQuicksand()) {
298 return id(new AphrontAjaxResponse())
299 ->setContent(
300 array(
301 'redirect' => $response->getURI(),
302 'close' => $response->getCloseDialogBeforeRedirect(),
307 return $response;
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())
318 ->withPHIDs($phids)
319 ->execute();
322 public function buildApplicationMenu() {
323 return null;
326 protected function buildApplicationCrumbs() {
327 $crumbs = array();
329 $application = $this->getCurrentApplication();
330 if ($application) {
331 $icon = $application->getIcon();
332 if (!$icon) {
333 $icon = 'fa-puzzle';
336 $crumbs[] = id(new PHUICrumbView())
337 ->setHref($this->getApplicationURI())
338 ->setName($application->getName())
339 ->setIcon($icon);
342 $view = new PHUICrumbsView();
343 foreach ($crumbs as $crumb) {
344 $view->addCrumb($crumb);
347 return $view;
350 protected function hasApplicationCapability($capability) {
351 return PhabricatorPolicyFilter::hasCapability(
352 $this->getRequest()->getUser(),
353 $this->getCurrentApplication(),
354 $capability);
357 protected function requireApplicationCapability($capability) {
358 PhabricatorPolicyFilter::requireCapability(
359 $this->getRequest()->getUser(),
360 $this->getCurrentApplication(),
361 $capability);
364 protected function explainApplicationCapability(
365 $capability,
366 $positive_message,
367 $negative_message) {
369 $can_act = $this->hasApplicationCapability($capability);
370 if ($can_act) {
371 $message = $positive_message;
372 $icon_name = 'fa-play-circle-o lightgreytext';
373 } else {
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(
387 'div',
388 array(
389 'class' => 'policy-capability-explanation',
391 array(
392 $icon,
393 javelin_tag(
394 'a',
395 array(
396 'href' => $explain_uri,
397 'sigil' => 'workflow',
399 $message),
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();
430 if ($application) {
431 $page->setApplicationName($application->getName());
432 if ($application->getTitleGlyph()) {
433 $page->setGlyph($application->getTitleGlyph());
437 $viewer = $this->getRequest()->getUser();
438 if ($viewer) {
439 $page->setUser($viewer);
442 return $page;
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())
456 ->setViewer($viewer)
457 ->setID($action_id);
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.
462 if ($object) {
463 if ($object instanceof PhabricatorLiskDAO) {
464 $action_list->setObject($object);
468 $curtain = id(new PHUICurtainView())
469 ->setViewer($viewer)
470 ->setActionList($action_list);
472 if ($object) {
473 $panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);
474 foreach ($panels as $panel) {
475 $curtain->addPanel($panel);
479 return $curtain;
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();
492 if (!$query) {
493 $query = PhabricatorApplicationTransactionQuery::newQueryForObject(
494 $object);
495 if (!$query) {
496 throw new Exception(
497 pht(
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().'/'));
508 $xactions = $query
509 ->setViewer($viewer)
510 ->withObjectPHIDs(array($object->getPHID()))
511 ->needComments(true)
512 ->executeWithCursorPager($pager);
513 $xactions = array_reverse($xactions);
515 $timeline_engine = PhabricatorTimelineEngine::newForObject($object)
516 ->setViewer($viewer)
517 ->setTransactions($xactions)
518 ->setViewData($view_data);
520 $view = $timeline_engine->buildTimelineView();
522 if ($engine) {
523 foreach ($xactions as $xaction) {
524 if ($xaction->getComment()) {
525 $engine->addObject(
526 $xaction->getComment(),
527 PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
530 $engine->process();
531 $view->setMarkupEngine($engine);
534 $timeline = $view
535 ->setPager($pager)
536 ->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
537 ->setQuoteRef($this->getRequest()->getStr('quoteRef'));
539 return $timeline;
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()) {
552 return null;
555 if ($this->shouldAllowLegallyNonCompliantUsers()) {
556 return null;
559 $viewer = $this->getViewer();
561 if (!$viewer->hasSession()) {
562 return null;
565 $session = $viewer->getSession();
566 if ($session->getIsPartial()) {
567 // If the user hasn't made it through MFA yet, require they survive
568 // MFA first.
569 return null;
572 if ($session->getSignedLegalpadDocuments()) {
573 return null;
576 if (!$viewer->isLoggedIn()) {
577 return null;
580 $must_sign_docs = array();
581 $sign_docs = array();
583 $legalpad_class = 'PhabricatorLegalpadApplication';
584 $legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(
585 $legalpad_class,
586 $viewer);
587 if ($legalpad_installed) {
588 $sign_docs = id(new LegalpadDocumentQuery())
589 ->setViewer($viewer)
590 ->withSignatureRequired(1)
591 ->needViewerSignatures(true)
592 ->setOrder('oldest')
593 ->execute();
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);
609 return null;
612 $request = $this->getRequest();
613 $request->setURIMap(
614 array(
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();