@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
at recaptime-dev/main 645 lines 19 kB view raw
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}