diff --git a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php index 44b58b85ff..4a4babcc12 100644 --- a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php @@ -1,493 +1,494 @@ setProperty(self::KEY_PORT, 389) ->setProperty(self::KEY_VERSION, 3); } public function getAdapter() { if (!$this->adapter) { $conf = $this->getProviderConfig(); $realname_attributes = $conf->getProperty(self::KEY_REALNAME_ATTRIBUTES); if (!is_array($realname_attributes)) { $realname_attributes = array(); } $search_attributes = $conf->getProperty(self::KEY_SEARCH_ATTRIBUTES); $search_attributes = phutil_split_lines($search_attributes, false); $search_attributes = array_filter($search_attributes); $adapter = id(new PhutilLDAPAuthAdapter()) ->setHostname( $conf->getProperty(self::KEY_HOSTNAME)) ->setPort( $conf->getProperty(self::KEY_PORT)) ->setBaseDistinguishedName( $conf->getProperty(self::KEY_DISTINGUISHED_NAME)) ->setSearchAttributes($search_attributes) ->setUsernameAttribute( $conf->getProperty(self::KEY_USERNAME_ATTRIBUTE)) ->setRealNameAttributes($realname_attributes) ->setLDAPVersion( $conf->getProperty(self::KEY_VERSION)) ->setLDAPReferrals( $conf->getProperty(self::KEY_REFERRALS)) ->setLDAPStartTLS( $conf->getProperty(self::KEY_START_TLS)) ->setAlwaysSearch($conf->getProperty(self::KEY_ALWAYS_SEARCH)) ->setAnonymousUsername( $conf->getProperty(self::KEY_ANONYMOUS_USERNAME)) ->setAnonymousPassword( new PhutilOpaqueEnvelope( $conf->getProperty(self::KEY_ANONYMOUS_PASSWORD))) ->setActiveDirectoryDomain( $conf->getProperty(self::KEY_ACTIVEDIRECTORY_DOMAIN)); $this->adapter = $adapter; } return $this->adapter; } protected function renderLoginForm(AphrontRequest $request, $mode) { $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setSubmitURI($this->getLoginURI()) ->setUser($viewer); if ($mode == 'link') { $dialog->setTitle(pht('Link LDAP Account')); $dialog->addSubmitButton(pht('Link Accounts')); $dialog->addCancelButton($this->getSettingsURI()); } else if ($mode == 'refresh') { $dialog->setTitle(pht('Refresh LDAP Account')); $dialog->addSubmitButton(pht('Refresh Account')); $dialog->addCancelButton($this->getSettingsURI()); } else { if ($this->shouldAllowRegistration()) { $dialog->setTitle(pht('Log In or Register with LDAP')); $dialog->addSubmitButton(pht('Log In or Register')); } else { $dialog->setTitle(pht('Log In with LDAP')); $dialog->addSubmitButton(pht('Log In')); } if ($mode == 'login') { $dialog->addCancelButton($this->getStartURI()); } } $v_user = $request->getStr('ldap_username'); $e_user = null; $e_pass = null; $errors = array(); if ($request->isHTTPPost()) { // NOTE: This is intentionally vague so as not to disclose whether a // given username exists. $e_user = pht('Invalid'); $e_pass = pht('Invalid'); $errors[] = pht('Username or password are incorrect.'); } $form = id(new PHUIFormLayoutView()) ->setUser($viewer) ->setFullWidth(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('LDAP Username')) ->setName('ldap_username') + ->setAutofocus(true) ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('LDAP Password')) ->setName('ldap_password') ->setError($e_pass)); if ($errors) { $errors = id(new PHUIInfoView())->setErrors($errors); } $dialog->appendChild($errors); $dialog->appendChild($form); return $dialog; } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $viewer = $request->getUser(); $response = null; $account = null; $username = $request->getStr('ldap_username'); $password = $request->getStr('ldap_password'); $has_password = strlen($password); $password = new PhutilOpaqueEnvelope($password); if (!strlen($username) || !$has_password) { $response = $controller->buildProviderPageResponse( $this, $this->renderLoginForm($request, 'login')); return array($account, $response); } if ($request->isFormPost()) { try { if (strlen($username) && $has_password) { $adapter = $this->getAdapter(); $adapter->setLoginUsername($username); $adapter->setLoginPassword($password); // TODO: This calls ldap_bind() eventually, which dumps cleartext // passwords to the error log. See note in PhutilLDAPAuthAdapter. // See T3351. DarkConsoleErrorLogPluginAPI::enableDiscardMode(); $account_id = $adapter->getAccountID(); DarkConsoleErrorLogPluginAPI::disableDiscardMode(); } else { throw new Exception(pht('Username and password are required!')); } } catch (PhutilAuthCredentialException $ex) { $response = $controller->buildProviderPageResponse( $this, $this->renderLoginForm($request, 'login')); return array($account, $response); } catch (Exception $ex) { // TODO: Make this cleaner. throw $ex; } } return array($this->loadOrCreateAccount($account_id), $response); } const KEY_HOSTNAME = 'ldap:host'; const KEY_PORT = 'ldap:port'; const KEY_DISTINGUISHED_NAME = 'ldap:dn'; const KEY_SEARCH_ATTRIBUTES = 'ldap:search-attribute'; const KEY_USERNAME_ATTRIBUTE = 'ldap:username-attribute'; const KEY_REALNAME_ATTRIBUTES = 'ldap:realname-attributes'; const KEY_VERSION = 'ldap:version'; const KEY_REFERRALS = 'ldap:referrals'; const KEY_START_TLS = 'ldap:start-tls'; // TODO: This is misspelled! See T13005. const KEY_ANONYMOUS_USERNAME = 'ldap:anoynmous-username'; const KEY_ANONYMOUS_PASSWORD = 'ldap:anonymous-password'; const KEY_ALWAYS_SEARCH = 'ldap:always-search'; const KEY_ACTIVEDIRECTORY_DOMAIN = 'ldap:activedirectory-domain'; private function getPropertyKeys() { return array_keys($this->getPropertyLabels()); } private function getPropertyLabels() { return array( self::KEY_HOSTNAME => pht('LDAP Hostname'), self::KEY_PORT => pht('LDAP Port'), self::KEY_DISTINGUISHED_NAME => pht('Base Distinguished Name'), self::KEY_SEARCH_ATTRIBUTES => pht('Search Attributes'), self::KEY_ALWAYS_SEARCH => pht('Always Search'), self::KEY_ANONYMOUS_USERNAME => pht('Anonymous Username'), self::KEY_ANONYMOUS_PASSWORD => pht('Anonymous Password'), self::KEY_USERNAME_ATTRIBUTE => pht('Username Attribute'), self::KEY_REALNAME_ATTRIBUTES => pht('Realname Attributes'), self::KEY_VERSION => pht('LDAP Version'), self::KEY_REFERRALS => pht('Enable Referrals'), self::KEY_START_TLS => pht('Use TLS'), self::KEY_ACTIVEDIRECTORY_DOMAIN => pht('ActiveDirectory Domain'), ); } public function readFormValuesFromProvider() { $properties = array(); foreach ($this->getPropertyLabels() as $key => $ignored) { $properties[$key] = $this->getProviderConfig()->getProperty($key); } return $properties; } public function readFormValuesFromRequest(AphrontRequest $request) { $values = array(); foreach ($this->getPropertyKeys() as $key) { switch ($key) { case self::KEY_REALNAME_ATTRIBUTES: $values[$key] = $request->getStrList($key, array()); break; default: $values[$key] = $request->getStr($key); break; } } return $values; } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public static function assertLDAPExtensionInstalled() { if (!function_exists('ldap_bind')) { throw new Exception( pht( 'Before you can set up or use LDAP, you need to install the PHP '. 'LDAP extension. It is not currently installed, so PHP can not '. 'talk to LDAP. Usually you can install it with '. '`%s`, `%s`, or a similar package manager command.', 'yum install php-ldap', 'apt-get install php5-ldap')); } } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { self::assertLDAPExtensionInstalled(); $labels = $this->getPropertyLabels(); $captions = array( self::KEY_HOSTNAME => pht('Example: %s%sFor LDAPS, use: %s', phutil_tag('tt', array(), pht('ldap.example.com')), phutil_tag('br'), phutil_tag('tt', array(), pht('ldaps://ldaps.example.com/'))), self::KEY_DISTINGUISHED_NAME => pht('Example: %s', phutil_tag('tt', array(), pht('ou=People, dc=example, dc=com'))), self::KEY_USERNAME_ATTRIBUTE => pht('Example: %s', phutil_tag('tt', array(), pht('sn'))), self::KEY_REALNAME_ATTRIBUTES => pht('Example: %s', phutil_tag('tt', array(), pht('firstname, lastname'))), self::KEY_REFERRALS => pht('Follow referrals. Disable this for Windows AD 2003.'), self::KEY_START_TLS => pht('Start TLS after binding to the LDAP server.'), self::KEY_ALWAYS_SEARCH => pht('Always bind and search, even without a username and password.'), ); $types = array( self::KEY_REFERRALS => 'checkbox', self::KEY_START_TLS => 'checkbox', self::KEY_SEARCH_ATTRIBUTES => 'textarea', self::KEY_REALNAME_ATTRIBUTES => 'list', self::KEY_ANONYMOUS_PASSWORD => 'password', self::KEY_ALWAYS_SEARCH => 'checkbox', ); $instructions = array( self::KEY_SEARCH_ATTRIBUTES => pht( "When a user types their LDAP username and password into Phabricator, ". "Phabricator can either bind to LDAP with those credentials directly ". "(which is simpler, but not as powerful) or bind to LDAP with ". "anonymous credentials, then search for record matching the supplied ". "credentials (which is more complicated, but more powerful).\n\n". "For many installs, direct binding is sufficient. However, you may ". "want to search first if:\n\n". " - You want users to be able to log in with either their username ". " or their email address.\n". " - The login/username is not part of the distinguished name in ". " your LDAP records.\n". " - You want to restrict logins to a subset of users (like only ". " those in certain departments).\n". " - Your LDAP server is configured in some other way that prevents ". " direct binding from working correctly.\n\n". "**To bind directly**, enter the LDAP attribute corresponding to the ". "login name into the **Search Attributes** box below. Often, this is ". "something like `sn` or `uid`. This is the simplest configuration, ". "but will only work if the username is part of the distinguished ". "name, and won't let you apply complex restrictions to logins.\n\n". " lang=text,name=Simple Direct Binding\n". " sn\n\n". "**To search first**, provide an anonymous username and password ". "below (or check the **Always Search** checkbox), then enter one ". "or more search queries into this field, one per line. ". "After binding, these queries will be used to identify the ". "record associated with the login name the user typed.\n\n". "Searches will be tried in order until a matching record is found. ". "Each query can be a simple attribute name (like `sn` or `mail`), ". "which will search for a matching record, or it can be a complex ". "query that uses the string `\${login}` to represent the login ". "name.\n\n". "A common simple configuration is just an attribute name, like ". "`sn`, which will work the same way direct binding works:\n\n". " lang=text,name=Simple Example\n". " sn\n\n". "A slightly more complex configuration might let the user log in with ". "either their login name or email address:\n\n". " lang=text,name=Match Several Attributes\n". " mail\n". " sn\n\n". "If your LDAP directory is more complex, or you want to perform ". "sophisticated filtering, you can use more complex queries. Depending ". "on your directory structure, this example might allow users to log ". "in with either their email address or username, but only if they're ". "in specific departments:\n\n". " lang=text,name=Complex Example\n". " (&(mail=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n". " (&(sn=\${login})(|(departmentNumber=1)(departmentNumber=2)))\n\n". "All of the attribute names used here are just examples: your LDAP ". "server may use different attribute names."), self::KEY_ALWAYS_SEARCH => pht( 'To search for an LDAP record before authenticating, either check '. 'the **Always Search** checkbox or enter an anonymous '. 'username and password to use to perform the search.'), self::KEY_USERNAME_ATTRIBUTE => pht( 'Optionally, specify a username attribute to use to prefill usernames '. 'when registering a new account. This is purely cosmetic and does not '. 'affect the login process, but you can configure it to make sure '. 'users get the same default username as their LDAP username, so '. 'usernames remain consistent across systems.'), self::KEY_REALNAME_ATTRIBUTES => pht( 'Optionally, specify one or more comma-separated attributes to use to '. 'prefill the "Real Name" field when registering a new account. This '. 'is purely cosmetic and does not affect the login process, but can '. 'make registration a little easier.'), ); foreach ($labels as $key => $label) { $caption = idx($captions, $key); $type = idx($types, $key); $value = idx($values, $key); $control = null; switch ($type) { case 'checkbox': $control = id(new AphrontFormCheckboxControl()) ->addCheckbox( $key, 1, hsprintf('%s: %s', $label, $caption), $value); break; case 'list': $control = id(new AphrontFormTextControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setValue($value ? implode(', ', $value) : null); break; case 'password': $control = id(new AphrontFormPasswordControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setDisableAutocomplete(true) ->setValue($value); break; case 'textarea': $control = id(new AphrontFormTextAreaControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setValue($value); break; default: $control = id(new AphrontFormTextControl()) ->setName($key) ->setLabel($label) ->setCaption($caption) ->setValue($value); break; } $instruction_text = idx($instructions, $key); if (strlen($instruction_text)) { $form->appendRemarkupInstructions($instruction_text); } $form->appendChild($control); } } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); $labels = $this->getPropertyLabels(); if (isset($labels[$key])) { $label = $labels[$key]; $mask = false; switch ($key) { case self::KEY_ANONYMOUS_PASSWORD: $mask = true; break; } if ($mask) { return pht( '%s updated the "%s" value.', $xaction->renderHandleLink($author_phid), $label); } if ($old === null || $old === '') { return pht( '%s set the "%s" value to "%s".', $xaction->renderHandleLink($author_phid), $label, $new); } else { return pht( '%s changed the "%s" value from "%s" to "%s".', $xaction->renderHandleLink($author_phid), $label, $old, $new); } } return parent::renderConfigPropertyTransactionTitle($xaction); } public static function getLDAPProvider() { $providers = self::getAllEnabledProviders(); foreach ($providers as $provider) { if ($provider instanceof PhabricatorLDAPAuthProvider) { return $provider; } } return null; } } diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index d841f091aa..ec5720e078 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -1,405 +1,406 @@ 'color: #009900', ), pht('Yes')); $no = phutil_tag( 'strong', array( 'style' => 'color: #990000', ), pht('Not Installed')); $best_hasher_name = null; try { $best_hasher = PhabricatorPasswordHasher::getBestHasher(); $best_hasher_name = $best_hasher->getHashName(); } catch (PhabricatorPasswordHasherUnavailableException $ex) { // There are no suitable hashers. The user might be able to enable some, // so we don't want to fatal here. We'll fatal when users try to actually // use this stuff if it isn't fixed before then. Until then, we just // don't highlight a row. In practice, at least one hasher should always // be available. } $rows = array(); $rowc = array(); foreach ($hashers as $hasher) { $is_installed = $hasher->canHashPasswords(); $rows[] = array( $hasher->getHumanReadableName(), $hasher->getHashName(), $hasher->getHumanReadableStrength(), ($is_installed ? $yes : $no), ($is_installed ? null : $hasher->getInstallInstructions()), ); $rowc[] = ($best_hasher_name == $hasher->getHashName()) ? 'highlighted' : null; } $table = new AphrontTableView($rows); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Algorithm'), pht('Name'), pht('Strength'), pht('Installed'), pht('Install Instructions'), )); $table->setColumnClasses( array( '', '', '', '', 'wide', )); $header = id(new PHUIHeaderView()) ->setHeader(pht('Password Hash Algorithms')) ->setSubheader( pht( 'Stronger algorithms are listed first. The highlighted algorithm '. 'will be used when storing new hashes. Older hashes will be '. 'upgraded to the best algorithm over time.')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setTable($table); } public function getDescriptionForCreate() { return pht( 'Allow users to log in or register using a username and password.'); } public function getAdapter() { if (!$this->adapter) { $adapter = new PhutilEmptyAuthAdapter(); $adapter->setAdapterType('password'); $adapter->setAdapterDomain('self'); $this->adapter = $adapter; } return $this->adapter; } public function getLoginOrder() { // Make sure username/password appears first if it is enabled. return '100-'.$this->getProviderName(); } public function shouldAllowAccountLink() { return false; } public function shouldAllowAccountUnlink() { return false; } public function isDefaultRegistrationProvider() { return true; } public function buildLoginForm( PhabricatorAuthStartController $controller) { $request = $controller->getRequest(); return $this->renderPasswordLoginForm($request); } public function buildInviteForm( PhabricatorAuthStartController $controller) { $request = $controller->getRequest(); $viewer = $request->getViewer(); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('invite', true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username')) ->setName('username')); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Register an Account')) ->appendForm($form) ->setSubmitURI('/auth/register/') ->addSubmitButton(pht('Continue')); return $dialog; } public function buildLinkForm( PhabricatorAuthLinkController $controller) { throw new Exception(pht("Password providers can't be linked.")); } private function renderPasswordLoginForm( AphrontRequest $request, $require_captcha = false, $captcha_valid = false) { $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setSubmitURI($this->getLoginURI()) ->setUser($viewer) ->setTitle(pht('Log In')) ->addSubmitButton(pht('Log In')); if ($this->shouldAllowRegistration()) { $dialog->addCancelButton( '/auth/register/', pht('Register New Account')); } $dialog->addFooter( phutil_tag( 'a', array( 'href' => '/login/email/', ), pht('Forgot your password?'))); $v_user = nonempty( $request->getStr('username'), $request->getCookie(PhabricatorCookies::COOKIE_USERNAME)); $e_user = null; $e_pass = null; $e_captcha = null; $errors = array(); if ($require_captcha && !$captcha_valid) { if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) { $e_captcha = pht('Invalid'); $errors[] = pht('CAPTCHA was not entered correctly.'); } else { $e_captcha = pht('Required'); $errors[] = pht( 'Too many login failures recently. You must '. 'submit a CAPTCHA with your login request.'); } } else if ($request->isHTTPPost()) { // NOTE: This is intentionally vague so as not to disclose whether a // given username or email is registered. $e_user = pht('Invalid'); $e_pass = pht('Invalid'); $errors[] = pht('Username or password are incorrect.'); } if ($errors) { $errors = id(new PHUIInfoView())->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->setFullWidth(true) ->appendChild($errors) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username or Email')) ->setName('username') + ->setAutofocus(true) ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password') ->setError($e_pass)); if ($require_captcha) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setError($e_captcha)); } $dialog->appendChild($form); return $dialog; } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $viewer = $request->getUser(); $content_source = PhabricatorContentSource::newFromRequest($request); $captcha_limit = 5; $hard_limit = 32; $limit_window = phutil_units('15 minutes in seconds'); $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( PhabricatorUserLog::ACTION_LOGIN_FAILURE, $limit_window); // If the same remote address has submitted several failed login attempts // recently, require they provide a CAPTCHA response for new attempts. $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { if (count($failed_attempts) > $captcha_limit) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } // If the user has submitted quite a few failed login attempts recently, // give them a hard limit. if (count($failed_attempts) > $hard_limit) { $guidance = array(); $guidance[] = pht( 'Your remote address has failed too many login attempts recently. '. 'Wait a few minutes before trying again.'); $guidance[] = pht( 'If you are unable to log in to your account, you can '. '[[ /login/email | send a reset link to your email address ]].'); $guidance = implode("\n\n", $guidance); $dialog = $controller->newDialog() ->setTitle(pht('Too Many Login Attempts')) ->appendChild(new PHUIRemarkupView($viewer, $guidance)) ->addCancelButton('/auth/start/', pht('Wait Patiently')); return array(null, $dialog); } $response = null; $account = null; $log_user = null; if ($request->isFormPost()) { if (!$require_captcha || $captcha_valid) { $username_or_email = $request->getStr('username'); if (strlen($username_or_email)) { $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username_or_email); if (!$user) { $user = PhabricatorUser::loadOneWithEmailAddress( $username_or_email); } if ($user) { $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); $engine = id(new PhabricatorAuthPasswordEngine()) ->setViewer($user) ->setContentSource($content_source) ->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT) ->setObject($user); if ($engine->isValidPassword($envelope)) { $account = $this->loadOrCreateAccount($user->getPHID()); $log_user = $user; } } } } } if (!$account) { if ($request->isFormPost()) { $log = PhabricatorUserLog::initializeNewLog( null, $log_user ? $log_user->getPHID() : null, PhabricatorUserLog::ACTION_LOGIN_FAILURE); $log->save(); } $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); $response = $controller->buildProviderPageResponse( $this, $this->renderPasswordLoginForm( $request, $require_captcha, $captcha_valid)); } return array($account, $response); } public function shouldRequireRegistrationPassword() { return true; } public function getDefaultExternalAccount() { $adapter = $this->getAdapter(); return id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()); } protected function willSaveAccount(PhabricatorExternalAccount $account) { parent::willSaveAccount($account); $account->setUserPHID($account->getAccountID()); } public function willRegisterAccount(PhabricatorExternalAccount $account) { parent::willRegisterAccount($account); $account->setAccountID($account->getUserPHID()); } public static function getPasswordProvider() { $providers = self::getAllEnabledProviders(); foreach ($providers as $provider) { if ($provider instanceof PhabricatorPasswordAuthProvider) { return $provider; } } return null; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { return; } public function shouldAllowAccountRefresh() { return false; } public function shouldAllowEmailTrustConfiguration() { return false; } } diff --git a/src/view/form/control/AphrontFormTextControl.php b/src/view/form/control/AphrontFormTextControl.php index 581f22682d..f7fd117cfd 100644 --- a/src/view/form/control/AphrontFormTextControl.php +++ b/src/view/form/control/AphrontFormTextControl.php @@ -1,55 +1,66 @@ disableAutocomplete = $disable; return $this; } private function getDisableAutocomplete() { return $this->disableAutocomplete; } public function getPlaceholder() { return $this->placeholder; } public function setPlaceholder($placeholder) { $this->placeholder = $placeholder; return $this; } + public function setAutofocus($autofocus) { + $this->autofocus = $autofocus; + return $this; + } + + public function getAutofocus() { + return $this->autofocus; + } + public function getSigil() { return $this->sigil; } public function setSigil($sigil) { $this->sigil = $sigil; return $this; } protected function getCustomControlClass() { return 'aphront-form-control-text'; } protected function renderInput() { return javelin_tag( 'input', array( 'type' => 'text', 'name' => $this->getName(), 'value' => $this->getValue(), 'disabled' => $this->getDisabled() ? 'disabled' : null, 'autocomplete' => $this->getDisableAutocomplete() ? 'off' : null, 'id' => $this->getID(), 'sigil' => $this->getSigil(), 'placeholder' => $this->getPlaceholder(), + 'autofocus' => ($this->getAutofocus() ? 'autofocus' : null), )); } }