Path: blob/master/src/applications/base/controller/PhabricatorController.php
13401 views
<?php12abstract class PhabricatorController extends AphrontController {34private $handles;56public function shouldRequireLogin() {7return true;8}910public function shouldRequireAdmin() {11return false;12}1314public function shouldRequireEnabledUser() {15return true;16}1718public function shouldAllowPublic() {19return false;20}2122public function shouldAllowPartialSessions() {23return false;24}2526public function shouldRequireEmailVerification() {27return PhabricatorUserEmail::isEmailVerificationRequired();28}2930public function shouldAllowRestrictedParameter($parameter_name) {31return false;32}3334public function shouldRequireMultiFactorEnrollment() {35if (!$this->shouldRequireLogin()) {36return false;37}3839if (!$this->shouldRequireEnabledUser()) {40return false;41}4243if ($this->shouldAllowPartialSessions()) {44return false;45}4647$user = $this->getRequest()->getUser();48if (!$user->getIsStandardUser()) {49return false;50}5152return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');53}5455public function shouldAllowLegallyNonCompliantUsers() {56return false;57}5859public function isGlobalDragAndDropUploadEnabled() {60return false;61}6263public function willBeginExecution() {64$request = $this->getRequest();6566if ($request->getUser()) {67// NOTE: Unit tests can set a user explicitly. Normal requests are not68// permitted to do this.69PhabricatorTestCase::assertExecutingUnitTests();70$user = $request->getUser();71} else {72$user = new PhabricatorUser();73$session_engine = new PhabricatorAuthSessionEngine();7475$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);76if ($phsid !== null && strlen($phsid)) {77$session_user = $session_engine->loadUserForSession(78PhabricatorAuthSession::TYPE_WEB,79$phsid);80if ($session_user) {81$user = $session_user;82}83} else {84// If the client doesn't have a session token, generate an anonymous85// session. This is used to provide CSRF protection to logged-out users.86$phsid = $session_engine->establishSession(87PhabricatorAuthSession::TYPE_WEB,88null,89$partial = false);9091// This may be a resource request, in which case we just don't set92// the cookie.93if ($request->canSetCookies()) {94$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);95}96}979899if (!$user->isLoggedIn()) {100$csrf = PhabricatorHash::digestWithNamedKey($phsid, 'csrf.alternate');101$user->attachAlternateCSRFString($csrf);102}103104$request->setUser($user);105}106107id(new PhabricatorAuthSessionEngine())108->willServeRequestForUser($user);109110if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {111$dark_console = PhabricatorDarkConsoleSetting::SETTINGKEY;112if ($user->getUserSetting($dark_console) ||113PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {114$console = new DarkConsoleCore();115$request->getApplicationConfiguration()->setConsole($console);116}117}118119// NOTE: We want to set up the user first so we can render a real page120// here, but fire this before any real logic.121$restricted = array(122'code',123);124foreach ($restricted as $parameter) {125if ($request->getExists($parameter)) {126if (!$this->shouldAllowRestrictedParameter($parameter)) {127throw new Exception(128pht(129'Request includes restricted parameter "%s", but this '.130'controller ("%s") does not whitelist it. Refusing to '.131'serve this request because it might be part of a redirection '.132'attack.',133$parameter,134get_class($this)));135}136}137}138139if ($this->shouldRequireEnabledUser()) {140if ($user->getIsDisabled()) {141$controller = new PhabricatorDisabledUserController();142return $this->delegateToController($controller);143}144}145146$auth_class = 'PhabricatorAuthApplication';147$auth_application = PhabricatorApplication::getByClass($auth_class);148149// Require partial sessions to finish login before doing anything.150if (!$this->shouldAllowPartialSessions()) {151if ($user->hasSession() &&152$user->getSession()->getIsPartial()) {153$login_controller = new PhabricatorAuthFinishController();154$this->setCurrentApplication($auth_application);155return $this->delegateToController($login_controller);156}157}158159// Require users sign Legalpad documents before we check if they have160// MFA. If we don't do this, they can get stuck in a state where they161// can't add MFA until they sign, and can't sign until they add MFA.162// See T13024 and PHI223.163$result = $this->requireLegalpadSignatures();164if ($result !== null) {165return $result;166}167168// Check if the user needs to configure MFA.169$need_mfa = $this->shouldRequireMultiFactorEnrollment();170$have_mfa = $user->getIsEnrolledInMultiFactor();171if ($need_mfa && !$have_mfa) {172// Check if the cache is just out of date. Otherwise, roadblock the user173// and require MFA enrollment.174$user->updateMultiFactorEnrollment();175if (!$user->getIsEnrolledInMultiFactor()) {176$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();177$this->setCurrentApplication($auth_application);178return $this->delegateToController($mfa_controller);179}180}181182if ($this->shouldRequireLogin()) {183// This actually means we need either:184// - a valid user, or a public controller; and185// - permission to see the application; and186// - permission to see at least one Space if spaces are configured.187188$allow_public = $this->shouldAllowPublic() &&189PhabricatorEnv::getEnvConfig('policy.allow-public');190191// If this controller isn't public, and the user isn't logged in, require192// login.193if (!$allow_public && !$user->isLoggedIn()) {194$login_controller = new PhabricatorAuthStartController();195$this->setCurrentApplication($auth_application);196return $this->delegateToController($login_controller);197}198199if ($user->isLoggedIn()) {200if ($this->shouldRequireEmailVerification()) {201if (!$user->getIsEmailVerified()) {202$controller = new PhabricatorMustVerifyEmailController();203$this->setCurrentApplication($auth_application);204return $this->delegateToController($controller);205}206}207}208209// If Spaces are configured, require that the user have access to at210// least one. If we don't do this, they'll get confusing error messages211// later on.212$spaces = PhabricatorSpacesNamespaceQuery::getSpacesExist();213if ($spaces) {214$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(215$user);216if (!$viewer_spaces) {217$controller = new PhabricatorSpacesNoAccessController();218return $this->delegateToController($controller);219}220}221222// If the user doesn't have access to the application, don't let them use223// any of its controllers. We query the application in order to generate224// a policy exception if the viewer doesn't have permission.225$application = $this->getCurrentApplication();226if ($application) {227id(new PhabricatorApplicationQuery())228->setViewer($user)229->withPHIDs(array($application->getPHID()))230->executeOne();231}232233// If users need approval, require they wait here. We do this near the234// end so they can take other actions (like verifying email, signing235// documents, and enrolling in MFA) while waiting for an admin to take a236// look at things. See T13024 for more discussion.237if ($this->shouldRequireEnabledUser()) {238if ($user->isLoggedIn() && !$user->getIsApproved()) {239$controller = new PhabricatorAuthNeedsApprovalController();240return $this->delegateToController($controller);241}242}243}244245// NOTE: We do this last so that users get a login page instead of a 403246// if they need to login.247if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {248return new Aphront403Response();249}250}251252public function getApplicationURI($path = '') {253if (!$this->getCurrentApplication()) {254throw new Exception(pht('No application!'));255}256return $this->getCurrentApplication()->getApplicationURI($path);257}258259public function willSendResponse(AphrontResponse $response) {260$request = $this->getRequest();261262if ($response instanceof AphrontDialogResponse) {263if (!$request->isAjax() && !$request->isQuicksand()) {264$dialog = $response->getDialog();265266$title = $dialog->getTitle();267$short = $dialog->getShortTitle();268269$crumbs = $this->buildApplicationCrumbs();270$crumbs->addTextCrumb(coalesce($short, $title));271272$page_content = array(273$crumbs,274$response->buildResponseString(),275);276277$view = id(new PhabricatorStandardPageView())278->setRequest($request)279->setController($this)280->setDeviceReady(true)281->setTitle($title)282->appendChild($page_content);283284$response = id(new AphrontWebpageResponse())285->setContent($view->render())286->setHTTPResponseCode($response->getHTTPResponseCode());287} else {288$response->getDialog()->setIsStandalone(true);289290return id(new AphrontAjaxResponse())291->setContent(array(292'dialog' => $response->buildResponseString(),293));294}295} else if ($response instanceof AphrontRedirectResponse) {296if ($request->isAjax() || $request->isQuicksand()) {297return id(new AphrontAjaxResponse())298->setContent(299array(300'redirect' => $response->getURI(),301'close' => $response->getCloseDialogBeforeRedirect(),302));303}304}305306return $response;307}308309/**310* WARNING: Do not call this in new code.311*312* @deprecated See "Handles Technical Documentation".313*/314protected function loadViewerHandles(array $phids) {315return id(new PhabricatorHandleQuery())316->setViewer($this->getRequest()->getUser())317->withPHIDs($phids)318->execute();319}320321public function buildApplicationMenu() {322return null;323}324325protected function buildApplicationCrumbs() {326$crumbs = array();327328$application = $this->getCurrentApplication();329if ($application) {330$icon = $application->getIcon();331if (!$icon) {332$icon = 'fa-puzzle';333}334335$crumbs[] = id(new PHUICrumbView())336->setHref($this->getApplicationURI())337->setName($application->getName())338->setIcon($icon);339}340341$view = new PHUICrumbsView();342foreach ($crumbs as $crumb) {343$view->addCrumb($crumb);344}345346return $view;347}348349protected function hasApplicationCapability($capability) {350return PhabricatorPolicyFilter::hasCapability(351$this->getRequest()->getUser(),352$this->getCurrentApplication(),353$capability);354}355356protected function requireApplicationCapability($capability) {357PhabricatorPolicyFilter::requireCapability(358$this->getRequest()->getUser(),359$this->getCurrentApplication(),360$capability);361}362363protected function explainApplicationCapability(364$capability,365$positive_message,366$negative_message) {367368$can_act = $this->hasApplicationCapability($capability);369if ($can_act) {370$message = $positive_message;371$icon_name = 'fa-play-circle-o lightgreytext';372} else {373$message = $negative_message;374$icon_name = 'fa-lock';375}376377$icon = id(new PHUIIconView())378->setIcon($icon_name);379380require_celerity_resource('policy-css');381382$phid = $this->getCurrentApplication()->getPHID();383$explain_uri = "/policy/explain/{$phid}/{$capability}/";384385$message = phutil_tag(386'div',387array(388'class' => 'policy-capability-explanation',389),390array(391$icon,392javelin_tag(393'a',394array(395'href' => $explain_uri,396'sigil' => 'workflow',397),398$message),399));400401return array($can_act, $message);402}403404public function getDefaultResourceSource() {405return 'phabricator';406}407408/**409* Create a new @{class:AphrontDialogView} with defaults filled in.410*411* @return AphrontDialogView New dialog.412*/413public function newDialog() {414$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());415$submit_uri = $submit_uri->getPath();416417return id(new AphrontDialogView())418->setUser($this->getRequest()->getUser())419->setSubmitURI($submit_uri);420}421422public function newRedirect() {423return id(new AphrontRedirectResponse());424}425426public function newPage() {427$page = id(new PhabricatorStandardPageView())428->setRequest($this->getRequest())429->setController($this)430->setDeviceReady(true);431432$application = $this->getCurrentApplication();433if ($application) {434$page->setApplicationName($application->getName());435if ($application->getTitleGlyph()) {436$page->setGlyph($application->getTitleGlyph());437}438}439440$viewer = $this->getRequest()->getUser();441if ($viewer) {442$page->setUser($viewer);443}444445return $page;446}447448public function newApplicationMenu() {449return id(new PHUIApplicationMenuView())450->setViewer($this->getViewer());451}452453public function newCurtainView($object = null) {454$viewer = $this->getViewer();455456$action_id = celerity_generate_unique_node_id();457458$action_list = id(new PhabricatorActionListView())459->setViewer($viewer)460->setID($action_id);461462// NOTE: Applications (objects of class PhabricatorApplication) can't463// currently be set here, although they don't need any of the extensions464// anyway. This should probably work differently than it does, though.465if ($object) {466if ($object instanceof PhabricatorLiskDAO) {467$action_list->setObject($object);468}469}470471$curtain = id(new PHUICurtainView())472->setViewer($viewer)473->setActionList($action_list);474475if ($object) {476$panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);477foreach ($panels as $panel) {478$curtain->addPanel($panel);479}480}481482return $curtain;483}484485protected function buildTransactionTimeline(486PhabricatorApplicationTransactionInterface $object,487PhabricatorApplicationTransactionQuery $query = null,488PhabricatorMarkupEngine $engine = null,489$view_data = array()) {490491$request = $this->getRequest();492$viewer = $this->getViewer();493$xaction = $object->getApplicationTransactionTemplate();494495if (!$query) {496$query = PhabricatorApplicationTransactionQuery::newQueryForObject(497$object);498if (!$query) {499throw new Exception(500pht(501'Unable to find transaction query for object of class "%s".',502get_class($object)));503}504}505506$pager = id(new AphrontCursorPagerView())507->readFromRequest($request)508->setURI(new PhutilURI(509'/transactions/showolder/'.$object->getPHID().'/'));510511$xactions = $query512->setViewer($viewer)513->withObjectPHIDs(array($object->getPHID()))514->needComments(true)515->executeWithCursorPager($pager);516$xactions = array_reverse($xactions);517518$timeline_engine = PhabricatorTimelineEngine::newForObject($object)519->setViewer($viewer)520->setTransactions($xactions)521->setViewData($view_data);522523$view = $timeline_engine->buildTimelineView();524525if ($engine) {526foreach ($xactions as $xaction) {527if ($xaction->getComment()) {528$engine->addObject(529$xaction->getComment(),530PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);531}532}533$engine->process();534$view->setMarkupEngine($engine);535}536537$timeline = $view538->setPager($pager)539->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))540->setQuoteRef($this->getRequest()->getStr('quoteRef'));541542return $timeline;543}544545546public function buildApplicationCrumbsForEditEngine() {547// TODO: This is kind of gross, I'm basically just making this public so548// I can use it in EditEngine. We could do this without making it public549// by using controller delegation, or make it properly public.550return $this->buildApplicationCrumbs();551}552553private function requireLegalpadSignatures() {554if (!$this->shouldRequireLogin()) {555return null;556}557558if ($this->shouldAllowLegallyNonCompliantUsers()) {559return null;560}561562$viewer = $this->getViewer();563564if (!$viewer->hasSession()) {565return null;566}567568$session = $viewer->getSession();569if ($session->getIsPartial()) {570// If the user hasn't made it through MFA yet, require they survive571// MFA first.572return null;573}574575if ($session->getSignedLegalpadDocuments()) {576return null;577}578579if (!$viewer->isLoggedIn()) {580return null;581}582583$must_sign_docs = array();584$sign_docs = array();585586$legalpad_class = 'PhabricatorLegalpadApplication';587$legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(588$legalpad_class,589$viewer);590if ($legalpad_installed) {591$sign_docs = id(new LegalpadDocumentQuery())592->setViewer($viewer)593->withSignatureRequired(1)594->needViewerSignatures(true)595->setOrder('oldest')596->execute();597598foreach ($sign_docs as $sign_doc) {599if (!$sign_doc->getUserSignature($viewer->getPHID())) {600$must_sign_docs[] = $sign_doc;601}602}603}604605if (!$must_sign_docs) {606// If nothing needs to be signed (either because there are no documents607// which require a signature, or because the user has already signed608// all of them) mark the session as good and continue.609$engine = id(new PhabricatorAuthSessionEngine())610->signLegalpadDocuments($viewer, $sign_docs);611612return null;613}614615$request = $this->getRequest();616$request->setURIMap(617array(618'id' => head($must_sign_docs)->getID(),619));620621$application = PhabricatorApplication::getByClass($legalpad_class);622$this->setCurrentApplication($application);623624$controller = new LegalpadDocumentSignController();625$controller->setIsSessionGate(true);626return $this->delegateToController($controller);627}628629630/* -( Deprecated )--------------------------------------------------------- */631632633/**634* DEPRECATED. Use @{method:newPage}.635*/636public function buildStandardPageView() {637return $this->newPage();638}639640641/**642* DEPRECATED. Use @{method:newPage}.643*/644public function buildStandardPageResponse($view, array $data) {645$page = $this->buildStandardPageView();646$page->appendChild($view);647return $page->produceAphrontResponse();648}649650}651652653