@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3abstract class PhabricatorController extends AphrontController {
4
5 private $handles;
6
7 public function shouldRequireLogin() {
8 return true;
9 }
10
11 public function shouldRequireAdmin() {
12 return false;
13 }
14
15 public function shouldRequireEnabledUser() {
16 return true;
17 }
18
19 public function shouldAllowPublic() {
20 return false;
21 }
22
23 public function shouldAllowPartialSessions() {
24 return false;
25 }
26
27 public function shouldRequireEmailVerification() {
28 return PhabricatorUserEmail::isEmailVerificationRequired();
29 }
30
31 public function shouldAllowRestrictedParameter($parameter_name) {
32 return false;
33 }
34
35 public function shouldRequireMultiFactorEnrollment() {
36 if (!$this->shouldRequireLogin()) {
37 return false;
38 }
39
40 if (!$this->shouldRequireEnabledUser()) {
41 return false;
42 }
43
44 if ($this->shouldAllowPartialSessions()) {
45 return false;
46 }
47
48 $user = $this->getRequest()->getUser();
49 if (!$user->getIsStandardUser()) {
50 return false;
51 }
52
53 return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
54 }
55
56 public function shouldAllowLegallyNonCompliantUsers() {
57 return false;
58 }
59
60 public function isGlobalDragAndDropUploadEnabled() {
61 return false;
62 }
63
64 public function willBeginExecution() {
65 $request = $this->getRequest();
66
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();
75
76 $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
77 if (phutil_nonempty_string($phsid)) {
78 $session_user = $session_engine->loadUserForSession(
79 PhabricatorAuthSession::TYPE_WEB,
80 $phsid);
81 if ($session_user) {
82 $user = $session_user;
83 }
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);
91
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);
96 }
97 }
98
99
100 if (!$user->isLoggedIn()) {
101 $csrf = PhabricatorHash::digestWithNamedKey($phsid, 'csrf.alternate');
102 $user->attachAlternateCSRFString($csrf);
103 }
104
105 $request->setUser($user);
106 }
107
108 id(new PhabricatorAuthSessionEngine())
109 ->willServeRequestForUser($user);
110
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);
117 }
118 }
119
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',
124 );
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)));
136 }
137 }
138 }
139
140 if ($this->shouldRequireEnabledUser()) {
141 if ($user->getIsDisabled()) {
142 $controller = new PhabricatorDisabledUserController();
143 return $this->delegateToController($controller);
144 }
145 }
146
147 $auth_class = 'PhabricatorAuthApplication';
148 $auth_application = PhabricatorApplication::getByClass($auth_class);
149
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);
157 }
158 }
159
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;
167 }
168
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);
180 }
181 }
182
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.
188
189 $allow_public = $this->shouldAllowPublic() &&
190 PhabricatorEnv::getEnvConfig('policy.allow-public');
191
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);
198 }
199
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);
206 }
207 }
208 }
209
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);
220 }
221 }
222
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();
232 }
233
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);
242 }
243 }
244 }
245
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();
250 }
251 }
252
253 public function getApplicationURI($path = '') {
254 if (!$this->getCurrentApplication()) {
255 throw new Exception(pht('No application!'));
256 }
257 return $this->getCurrentApplication()->getApplicationURI($path);
258 }
259
260 public function willSendResponse(AphrontResponse $response) {
261 $request = $this->getRequest();
262
263 if ($response instanceof AphrontDialogResponse) {
264 if (!$request->isAjax() && !$request->isQuicksand()) {
265 $dialog = $response->getDialog();
266
267 $title = $dialog->getTitle();
268 $short = $dialog->getShortTitle();
269
270 $crumbs = $this->buildApplicationCrumbs();
271 $crumbs->addTextCrumb(coalesce($short, $title));
272
273 $page_content = array(
274 $crumbs,
275 $response->buildResponseString(),
276 );
277
278 $view = id(new PhabricatorStandardPageView())
279 ->setRequest($request)
280 ->setController($this)
281 ->setDeviceReady(true)
282 ->setTitle($title)
283 ->appendChild($page_content);
284
285 $response = id(new AphrontWebpageResponse())
286 ->setContent($view->render())
287 ->setHTTPResponseCode($response->getHTTPResponseCode());
288 } else {
289 $response->getDialog()->setIsStandalone(true);
290
291 return id(new AphrontAjaxResponse())
292 ->setContent(array(
293 'dialog' => $response->buildResponseString(),
294 ));
295 }
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(),
303 ));
304 }
305 }
306
307 return $response;
308 }
309
310 /**
311 * WARNING: Do not call this in new code.
312 *
313 * @deprecated See "Handles Technical Documentation".
314 */
315 protected function loadViewerHandles(array $phids) {
316 return id(new PhabricatorHandleQuery())
317 ->setViewer($this->getRequest()->getUser())
318 ->withPHIDs($phids)
319 ->execute();
320 }
321
322 public function buildApplicationMenu() {
323 return null;
324 }
325
326 /**
327 * @return PHUICrumbsView
328 */
329 protected function buildApplicationCrumbs() {
330 $crumbs = array();
331
332 $application = $this->getCurrentApplication();
333 if ($application) {
334 $icon = $application->getIcon();
335 if (!$icon) {
336 $icon = 'fa-puzzle';
337 }
338
339 $crumbs[] = id(new PHUICrumbView())
340 ->setHref($this->getApplicationURI())
341 ->setName($application->getName())
342 ->setIcon($icon);
343 }
344
345 $view = new PHUICrumbsView();
346 foreach ($crumbs as $crumb) {
347 $view->addCrumb($crumb);
348 }
349
350 return $view;
351 }
352
353 protected function hasApplicationCapability($capability) {
354 return PhabricatorPolicyFilter::hasCapability(
355 $this->getRequest()->getUser(),
356 $this->getCurrentApplication(),
357 $capability);
358 }
359
360 protected function requireApplicationCapability($capability) {
361 PhabricatorPolicyFilter::requireCapability(
362 $this->getRequest()->getUser(),
363 $this->getCurrentApplication(),
364 $capability);
365 }
366
367 protected function explainApplicationCapability(
368 $capability,
369 $positive_message,
370 $negative_message) {
371
372 $can_act = $this->hasApplicationCapability($capability);
373 if ($can_act) {
374 $message = $positive_message;
375 $icon_name = 'fa-play-circle-o lightgreytext';
376 } else {
377 $message = $negative_message;
378 $icon_name = 'fa-lock';
379 }
380
381 $icon = id(new PHUIIconView())
382 ->setIcon($icon_name);
383
384 require_celerity_resource('policy-css');
385
386 $phid = $this->getCurrentApplication()->getPHID();
387 $explain_uri = "/policy/explain/{$phid}/{$capability}/";
388
389 $message = phutil_tag(
390 'div',
391 array(
392 'class' => 'policy-capability-explanation',
393 ),
394 array(
395 $icon,
396 javelin_tag(
397 'a',
398 array(
399 'href' => $explain_uri,
400 'sigil' => 'workflow',
401 ),
402 $message),
403 ));
404
405 return array($can_act, $message);
406 }
407
408 public function getDefaultResourceSource() {
409 return 'phabricator';
410 }
411
412 /**
413 * Create a new @{class:AphrontDialogView} with defaults filled in.
414 *
415 * @return AphrontDialogView New dialog.
416 */
417 public function newDialog() {
418 $submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
419 $submit_uri = $submit_uri->getPath();
420
421 return id(new AphrontDialogView())
422 ->setViewer($this->getRequest()->getUser())
423 ->setSubmitURI($submit_uri);
424 }
425
426 /**
427 * @return AphrontRedirectResponse
428 */
429 public function newRedirect() {
430 return id(new AphrontRedirectResponse());
431 }
432
433 /**
434 * @return PhabricatorStandardPageView
435 */
436 public function newPage() {
437 $page = id(new PhabricatorStandardPageView())
438 ->setRequest($this->getRequest())
439 ->setController($this)
440 ->setDeviceReady(true);
441
442 $application = $this->getCurrentApplication();
443 if ($application) {
444 $page->setApplicationName($application->getName());
445 if ($application->getTitleGlyph()) {
446 $page->setGlyph($application->getTitleGlyph());
447 }
448 }
449
450 $viewer = $this->getRequest()->getUser();
451 if ($viewer) {
452 $page->setViewer($viewer);
453 }
454
455 return $page;
456 }
457
458 /**
459 * @return PHUIApplicationMenuView
460 */
461 public function newApplicationMenu() {
462 return id(new PHUIApplicationMenuView())
463 ->setViewer($this->getViewer());
464 }
465
466 /**
467 * @return PHUICurtainView
468 */
469 public function newCurtainView($object = null) {
470 $viewer = $this->getViewer();
471
472 $action_id = celerity_generate_unique_node_id();
473
474 $action_list = id(new PhabricatorActionListView())
475 ->setViewer($viewer)
476 ->setID($action_id);
477
478 // NOTE: Applications (objects of class PhabricatorApplication) can't
479 // currently be set here, although they don't need any of the extensions
480 // anyway. This should probably work differently than it does, though.
481 if ($object) {
482 if ($object instanceof PhabricatorLiskDAO) {
483 $action_list->setObject($object);
484 }
485 }
486
487 $curtain = id(new PHUICurtainView())
488 ->setViewer($viewer)
489 ->setActionList($action_list);
490
491 if ($object) {
492 $panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);
493 foreach ($panels as $panel) {
494 $curtain->addPanel($panel);
495 }
496 }
497
498 return $curtain;
499 }
500
501 protected function buildTransactionTimeline(
502 PhabricatorApplicationTransactionInterface $object,
503 ?PhabricatorApplicationTransactionQuery $query = null,
504 ?PhabricatorMarkupEngine $engine = null,
505 $view_data = array()) {
506
507 $request = $this->getRequest();
508 $viewer = $this->getViewer();
509 $xaction = $object->getApplicationTransactionTemplate();
510
511 if (!$query) {
512 $query = PhabricatorApplicationTransactionQuery::newQueryForObject(
513 $object);
514 if (!$query) {
515 throw new Exception(
516 pht(
517 'Unable to find transaction query for object of class "%s".',
518 get_class($object)));
519 }
520 }
521
522 $pager = id(new AphrontCursorPagerView())
523 ->readFromRequest($request)
524 ->setURI(new PhutilURI(
525 '/transactions/showolder/'.$object->getPHID().'/'));
526
527 $xactions = $query
528 ->setViewer($viewer)
529 ->withObjectPHIDs(array($object->getPHID()))
530 ->needComments(true)
531 ->executeWithCursorPager($pager);
532 $xactions = array_reverse($xactions);
533
534 $timeline_engine = PhabricatorTimelineEngine::newForObject($object)
535 ->setViewer($viewer)
536 ->setTransactions($xactions)
537 ->setViewData($view_data);
538
539 $view = $timeline_engine->buildTimelineView();
540
541 if ($engine) {
542 foreach ($xactions as $xaction) {
543 if ($xaction->getComment()) {
544 $engine->addObject(
545 $xaction->getComment(),
546 PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
547 }
548 }
549 $engine->process();
550 $view->setMarkupEngine($engine);
551 }
552
553 $timeline = $view
554 ->setPager($pager)
555 ->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
556 ->setQuoteRef($this->getRequest()->getStr('quoteRef'));
557
558 return $timeline;
559 }
560
561
562 public function buildApplicationCrumbsForEditEngine() {
563 // TODO: This is kind of gross, I'm basically just making this public so
564 // I can use it in EditEngine. We could do this without making it public
565 // by using controller delegation, or make it properly public.
566 return $this->buildApplicationCrumbs();
567 }
568
569 private function requireLegalpadSignatures() {
570 if (!$this->shouldRequireLogin()) {
571 return null;
572 }
573
574 if ($this->shouldAllowLegallyNonCompliantUsers()) {
575 return null;
576 }
577
578 $viewer = $this->getViewer();
579
580 if (!$viewer->hasSession()) {
581 return null;
582 }
583
584 $session = $viewer->getSession();
585 if ($session->getIsPartial()) {
586 // If the user hasn't made it through MFA yet, require they survive
587 // MFA first.
588 return null;
589 }
590
591 if ($session->getSignedLegalpadDocuments()) {
592 return null;
593 }
594
595 if (!$viewer->isLoggedIn()) {
596 return null;
597 }
598
599 $must_sign_docs = array();
600 $sign_docs = array();
601
602 $legalpad_class = PhabricatorLegalpadApplication::class;
603 $legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(
604 $legalpad_class,
605 $viewer);
606 if ($legalpad_installed) {
607 $sign_docs = id(new LegalpadDocumentQuery())
608 ->setViewer($viewer)
609 ->withSignatureRequired(1)
610 ->needViewerSignatures(true)
611 ->setOrder('oldest')
612 ->execute();
613
614 foreach ($sign_docs as $sign_doc) {
615 if (!$sign_doc->getUserSignature($viewer->getPHID())) {
616 $must_sign_docs[] = $sign_doc;
617 }
618 }
619 }
620
621 if (!$must_sign_docs) {
622 // If nothing needs to be signed (either because there are no documents
623 // which require a signature, or because the user has already signed
624 // all of them) mark the session as good and continue.
625 id(new PhabricatorAuthSessionEngine())
626 ->signLegalpadDocuments($viewer, $sign_docs);
627
628 return null;
629 }
630
631 $request = $this->getRequest();
632 $request->setURIMap(
633 array(
634 'id' => head($must_sign_docs)->getID(),
635 ));
636
637 $application = PhabricatorApplication::getByClass($legalpad_class);
638 $this->setCurrentApplication($application);
639
640 $controller = new LegalpadDocumentSignController();
641 $controller->setIsSessionGate(true);
642 return $this->delegateToController($controller);
643 }
644
645}