@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 upstream/main 343 lines 11 kB view raw
1<?php 2 3final class PhabricatorAuthStartController 4 extends PhabricatorAuthController { 5 6 public function shouldRequireLogin() { 7 return false; 8 } 9 10 public function handleRequest(AphrontRequest $request) { 11 $viewer = $request->getUser(); 12 13 if ($viewer->isLoggedIn()) { 14 // Kick the user home if they are already logged in. 15 return id(new AphrontRedirectResponse())->setURI('/'); 16 } 17 18 if ($request->isAjax()) { 19 return $this->processAjaxRequest(); 20 } 21 22 if ($request->isConduit()) { 23 return $this->processConduitRequest(); 24 } 25 26 // If the user gets this far, they aren't logged in, so if they have a 27 // user session token we can conclude that it's invalid: if it was valid, 28 // they'd have been logged in above and never made it here. Try to clear 29 // it and warn the user they may need to nuke their cookies. 30 31 $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); 32 $did_clear = $request->getStr('cleared'); 33 34 if (phutil_nonempty_string($session_token)) { 35 $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken( 36 $session_token); 37 switch ($kind) { 38 case PhabricatorAuthSessionEngine::KIND_ANONYMOUS: 39 // If this is an anonymous session. It's expected that they won't 40 // be logged in, so we can just continue. 41 break; 42 default: 43 // The session cookie is invalid, so try to clear it. 44 $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); 45 $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); 46 47 // We've previously tried to clear the cookie but we ended up back 48 // here, so it didn't work. Hard fatal instead of trying again. 49 if ($did_clear) { 50 return $this->renderError( 51 pht( 52 'Your login session is invalid, and clearing the session '. 53 'cookie was unsuccessful. Try clearing your browser cookies.')); 54 } 55 56 $redirect_uri = $request->getRequestURI(); 57 $redirect_uri->replaceQueryParam('cleared', 1); 58 return id(new AphrontRedirectResponse())->setURI($redirect_uri); 59 } 60 } 61 62 // If we just cleared the session cookie and it worked, clean up after 63 // ourselves by redirecting to get rid of the "cleared" parameter. The 64 // the workflow will continue normally. 65 if ($did_clear) { 66 $redirect_uri = $request->getRequestURI(); 67 $redirect_uri->removeQueryParam('cleared'); 68 return id(new AphrontRedirectResponse())->setURI($redirect_uri); 69 } 70 71 $providers = PhabricatorAuthProvider::getAllEnabledProviders(); 72 foreach ($providers as $key => $provider) { 73 if (!$provider->shouldAllowLogin()) { 74 unset($providers[$key]); 75 } 76 } 77 78 $configs = array(); 79 foreach ($providers as $provider) { 80 $configs[] = $provider->getProviderConfig(); 81 } 82 83 if (!$providers) { 84 if ($this->isFirstTimeSetup()) { 85 // If this is a fresh install, let the user register their admin 86 // account. 87 return id(new AphrontRedirectResponse()) 88 ->setURI($this->getApplicationURI('/register/')); 89 } 90 91 return $this->renderError( 92 pht( 93 'This server is not configured with any enabled authentication '. 94 'providers which can be used to log in. If you have accidentally '. 95 'locked yourself out by disabling all providers, you can use `%s` '. 96 'to recover access to an account.', 97 './bin/auth recover <username>')); 98 } 99 100 $next_uri = $request->getStr('next'); 101 if (!phutil_nonempty_string($next_uri)) { 102 if ($this->getDelegatingController()) { 103 // Only set a next URI from the request path if this controller was 104 // delegated to, which happens when a user tries to view a page which 105 // requires them to login. 106 107 // If this controller handled the request directly, we're on the main 108 // login page, and never want to redirect the user back here after they 109 // login. 110 $next_uri = (string)$this->getRequest()->getRequestURI(); 111 } 112 } 113 114 if (!$request->isFormPost()) { 115 if (phutil_nonempty_string($next_uri)) { 116 PhabricatorCookies::setNextURICookie($request, $next_uri); 117 } 118 PhabricatorCookies::setClientIDCookie($request); 119 } 120 121 $auto_response = $this->tryAutoLogin($providers); 122 if ($auto_response) { 123 return $auto_response; 124 } 125 126 $invite = $this->loadInvite(); 127 128 $not_buttons = array(); 129 $are_buttons = array(); 130 $providers = msort($providers, 'getLoginOrder'); 131 foreach ($providers as $provider) { 132 if ($invite) { 133 $form = $provider->buildInviteForm($this); 134 } else { 135 $form = $provider->buildLoginForm($this); 136 } 137 if ($provider->isLoginFormAButton()) { 138 $are_buttons[] = $form; 139 } else { 140 $not_buttons[] = $form; 141 } 142 } 143 144 $out = array(); 145 $out[] = $not_buttons; 146 if ($are_buttons) { 147 require_celerity_resource('auth-css'); 148 149 foreach ($are_buttons as $key => $button) { 150 $are_buttons[$key] = phutil_tag( 151 'div', 152 array( 153 'class' => 'phabricator-login-button mmb', 154 ), 155 $button); 156 } 157 158 // If we only have one button, add a second pretend button so that we 159 // always have two columns. This makes it easier to get the alignments 160 // looking reasonable. 161 if (count($are_buttons) == 1) { 162 $are_buttons[] = null; 163 } 164 165 $button_columns = id(new AphrontMultiColumnView()) 166 ->setFluidLayout(true); 167 $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2)); 168 foreach ($are_buttons as $column) { 169 $button_columns->addColumn($column); 170 } 171 172 $out[] = phutil_tag( 173 'div', 174 array( 175 'class' => 'phabricator-login-buttons', 176 ), 177 $button_columns); 178 } 179 180 $invite_message = null; 181 if ($invite) { 182 $invite_message = $this->renderInviteHeader($invite); 183 } 184 185 $custom_message = $this->newCustomStartMessage(); 186 187 $email_login = $this->newEmailLoginView($configs); 188 189 $crumbs = $this->buildApplicationCrumbs(); 190 $crumbs->addTextCrumb(pht('Login')); 191 $crumbs->setBorder(true); 192 193 $title = pht('Login'); 194 $view = array( 195 $invite_message, 196 $custom_message, 197 $out, 198 $email_login, 199 ); 200 201 return $this->newPage() 202 ->setTitle($title) 203 ->setCrumbs($crumbs) 204 ->appendChild($view); 205 } 206 207 208 private function processAjaxRequest() { 209 $request = $this->getRequest(); 210 $viewer = $request->getUser(); 211 212 // We end up here if the user clicks a workflow link that they need to 213 // login to use. We give them a dialog saying "You need to login...". 214 215 if ($request->isDialogFormPost()) { 216 return id(new AphrontRedirectResponse())->setURI( 217 $request->getRequestURI()); 218 } 219 220 // Often, users end up here by clicking a disabled action link in the UI 221 // (for example, they might click "Edit Subtasks" on a Maniphest task 222 // page). After they log in we want to send them back to that main object 223 // page if we can, since it's confusing to end up on a standalone page with 224 // only a dialog (particularly if that dialog is another error, 225 // like a policy exception). 226 227 $via_header = AphrontRequest::getViaHeaderName(); 228 $via_uri = AphrontRequest::getHTTPHeader($via_header); 229 if (strlen($via_uri)) { 230 PhabricatorCookies::setNextURICookie($request, $via_uri, $force = true); 231 } 232 233 return $this->newDialog() 234 ->setTitle(pht('Login Required')) 235 ->appendParagraph(pht('You must log in to take this action.')) 236 ->addSubmitButton(pht('Log In')) 237 ->addCancelButton('/'); 238 } 239 240 241 private function processConduitRequest() { 242 $request = $this->getRequest(); 243 $viewer = $request->getUser(); 244 245 // A common source of errors in Conduit client configuration is getting 246 // the request path wrong. The client will end up here, so make some 247 // effort to give them a comprehensible error message. 248 249 $request_path = $this->getRequest()->getPath(); 250 $conduit_path = '/api/<method>'; 251 $example_path = '/api/conduit.ping'; 252 253 $message = pht( 254 'ERROR: You are making a Conduit API request to "%s", but the correct '. 255 'HTTP request path to use in order to access a Conduit method is "%s" '. 256 '(for example, "%s"). Check your configuration.', 257 $request_path, 258 $conduit_path, 259 $example_path); 260 261 return id(new AphrontPlainTextResponse())->setContent($message); 262 } 263 264 protected function renderError($message) { 265 return $this->renderErrorPage( 266 pht('Authentication Failure'), 267 array($message)); 268 } 269 270 private function tryAutoLogin(array $providers) { 271 $request = $this->getRequest(); 272 273 // If the user just logged out, don't immediately log them in again. 274 if ($request->getURIData('loggedout')) { 275 return null; 276 } 277 278 // If we have more than one provider, we can't autologin because we 279 // don't know which one the user wants. 280 if (count($providers) != 1) { 281 return null; 282 } 283 284 $provider = head($providers); 285 if (!$provider->supportsAutoLogin()) { 286 return null; 287 } 288 289 $config = $provider->getProviderConfig(); 290 if (!$config->getShouldAutoLogin()) { 291 return null; 292 } 293 294 $auto_uri = $provider->getAutoLoginURI($request); 295 296 return id(new AphrontRedirectResponse()) 297 ->setIsExternal(true) 298 ->setURI($auto_uri); 299 } 300 301 /** 302 * @param array<PhabricatorAuthProviderConfig> $configs 303 */ 304 private function newEmailLoginView(array $configs) { 305 assert_instances_of($configs, PhabricatorAuthProviderConfig::class); 306 307 // Check if password auth is enabled. If it is, the password login form 308 // renders a "Forgot password?" link, so we don't need to provide a 309 // supplemental link. 310 311 $has_password = false; 312 foreach ($configs as $config) { 313 $provider = $config->getProvider(); 314 if ($provider instanceof PhabricatorPasswordAuthProvider) { 315 $has_password = true; 316 } 317 } 318 319 if ($has_password) { 320 return null; 321 } 322 323 $view = array( 324 pht('Trouble logging in?'), 325 ' ', 326 phutil_tag( 327 'a', 328 array( 329 'href' => '/login/email/', 330 ), 331 pht('Send a login link to your email address.')), 332 ); 333 334 return phutil_tag( 335 'div', 336 array( 337 'class' => 'auth-custom-message', 338 ), 339 $view); 340 } 341 342 343}