diff --git a/resources/sql/patches/20130619.authconf.php b/resources/sql/patches/20130619.authconf.php index 21213b2ecb..0b56735521 100644 --- a/resources/sql/patches/20130619.authconf.php +++ b/resources/sql/patches/20130619.authconf.php @@ -1,164 +1,164 @@ array( + 'PhabricatorLDAPAuthProvider' => array( 'enabled' => 'ldap.auth-enabled', 'registration' => true, 'type' => 'ldap', 'domain' => 'self', ), 'PhabricatorAuthProviderOAuthDisqus' => array( 'enabled' => 'disqus.auth-enabled', 'registration' => 'disqus.registration-enabled', 'permanent' => 'disqus.auth-permanent', 'oauth.id' => 'disqus.application-id', 'oauth.secret' => 'disqus.application-secret', 'type' => 'disqus', 'domain' => 'disqus.com', ), - 'PhabricatorAuthProviderOAuthFacebook' => array( + 'PhabricatorFacebookAuthProvider' => array( 'enabled' => 'facebook.auth-enabled', 'registration' => 'facebook.registration-enabled', 'permanent' => 'facebook.auth-permanent', 'oauth.id' => 'facebook.application-id', 'oauth.secret' => 'facebook.application-secret', 'type' => 'facebook', 'domain' => 'facebook.com', ), 'PhabricatorAuthProviderOAuthGitHub' => array( 'enabled' => 'github.auth-enabled', 'registration' => 'github.registration-enabled', 'permanent' => 'github.auth-permanent', 'oauth.id' => 'github.application-id', 'oauth.secret' => 'github.application-secret', 'type' => 'github', 'domain' => 'github.com', ), 'PhabricatorAuthProviderOAuthGoogle' => array( 'enabled' => 'google.auth-enabled', 'registration' => 'google.registration-enabled', 'permanent' => 'google.auth-permanent', 'oauth.id' => 'google.application-id', 'oauth.secret' => 'google.application-secret', 'type' => 'google', 'domain' => 'google.com', ), - 'PhabricatorAuthProviderPassword' => array( + 'PhabricatorPasswordAuthProvider' => array( 'enabled' => 'auth.password-auth-enabled', 'enabled-default' => false, 'registration' => false, 'type' => 'password', 'domain' => 'self', ), ); foreach ($config_map as $provider_class => $spec) { $enabled_key = idx($spec, 'enabled'); $enabled_default = idx($spec, 'enabled-default', false); $enabled = PhabricatorEnv::getEnvConfigIfExists( $enabled_key, $enabled_default); if (!$enabled) { echo pht("Skipping %s (not enabled).\n", $provider_class); // This provider was not previously enabled, so we can skip migrating it. continue; } else { echo pht("Migrating %s...\n", $provider_class); } $registration_key = idx($spec, 'registration'); if ($registration_key === true) { $registration = 1; } else if ($registration_key === false) { $registration = 0; } else { $registration = (int)PhabricatorEnv::getEnvConfigIfExists( $registration_key, true); } $unlink_key = idx($spec, 'permanent'); if (!$unlink_key) { $unlink = 1; } else { $unlink = (int)(!PhabricatorEnv::getEnvConfigIfExists($unlink_key)); } $config = id(new PhabricatorAuthProviderConfig()) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration($registration) ->setShouldAllowLink(1) ->setShouldAllowUnlink($unlink) ->setProviderType(idx($spec, 'type')) ->setProviderDomain(idx($spec, 'domain')) ->setProviderClass($provider_class); if (isset($spec['oauth.id'])) { $config->setProperty( PhabricatorAuthProviderOAuth::PROPERTY_APP_ID, PhabricatorEnv::getEnvConfigIfExists(idx($spec, 'oauth.id'))); $config->setProperty( PhabricatorAuthProviderOAuth::PROPERTY_APP_SECRET, PhabricatorEnv::getEnvConfigIfExists(idx($spec, 'oauth.secret'))); } switch ($provider_class) { - case 'PhabricatorAuthProviderOAuthFacebook': + case 'PhabricatorFacebookAuthProvider': $config->setProperty( - PhabricatorAuthProviderOAuthFacebook::KEY_REQUIRE_SECURE, + PhabricatorFacebookAuthProvider::KEY_REQUIRE_SECURE, (int)PhabricatorEnv::getEnvConfigIfExists( 'facebook.require-https-auth')); break; - case 'PhabricatorAuthProviderLDAP': + case 'PhabricatorLDAPAuthProvider': $ldap_map = array( - PhabricatorAuthProviderLDAP::KEY_HOSTNAME + PhabricatorLDAPAuthProvider::KEY_HOSTNAME => 'ldap.hostname', - PhabricatorAuthProviderLDAP::KEY_PORT + PhabricatorLDAPAuthProvider::KEY_PORT => 'ldap.port', - PhabricatorAuthProviderLDAP::KEY_DISTINGUISHED_NAME + PhabricatorLDAPAuthProvider::KEY_DISTINGUISHED_NAME => 'ldap.base_dn', - PhabricatorAuthProviderLDAP::KEY_SEARCH_ATTRIBUTES + PhabricatorLDAPAuthProvider::KEY_SEARCH_ATTRIBUTES => 'ldap.search_attribute', - PhabricatorAuthProviderLDAP::KEY_USERNAME_ATTRIBUTE + PhabricatorLDAPAuthProvider::KEY_USERNAME_ATTRIBUTE => 'ldap.username-attribute', - PhabricatorAuthProviderLDAP::KEY_REALNAME_ATTRIBUTES + PhabricatorLDAPAuthProvider::KEY_REALNAME_ATTRIBUTES => 'ldap.real_name_attributes', - PhabricatorAuthProviderLDAP::KEY_VERSION + PhabricatorLDAPAuthProvider::KEY_VERSION => 'ldap.version', - PhabricatorAuthProviderLDAP::KEY_REFERRALS + PhabricatorLDAPAuthProvider::KEY_REFERRALS => 'ldap.referrals', - PhabricatorAuthProviderLDAP::KEY_START_TLS + PhabricatorLDAPAuthProvider::KEY_START_TLS => 'ldap.start-tls', - PhabricatorAuthProviderLDAP::KEY_ANONYMOUS_USERNAME + PhabricatorLDAPAuthProvider::KEY_ANONYMOUS_USERNAME => 'ldap.anonymous-user-name', - PhabricatorAuthProviderLDAP::KEY_ANONYMOUS_PASSWORD + PhabricatorLDAPAuthProvider::KEY_ANONYMOUS_PASSWORD => 'ldap.anonymous-user-password', // Update the old "search first" setting to the newer but similar // "always search" setting. - PhabricatorAuthProviderLDAP::KEY_ALWAYS_SEARCH + PhabricatorLDAPAuthProvider::KEY_ALWAYS_SEARCH => 'ldap.search-first', - PhabricatorAuthProviderLDAP::KEY_ACTIVEDIRECTORY_DOMAIN + PhabricatorLDAPAuthProvider::KEY_ACTIVEDIRECTORY_DOMAIN => 'ldap.activedirectory_domain', ); $defaults = array( 'ldap.version' => 3, 'ldap.port' => 389, ); foreach ($ldap_map as $pkey => $ckey) { $default = idx($defaults, $ckey); $config->setProperty( $pkey, PhabricatorEnv::getEnvConfigIfExists($ckey, $default)); } break; } $config->save(); } echo "Done.\n"; diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index cb663344b2..12decda80d 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -1,190 +1,190 @@ linkType = $data['type']; $this->id = $data['id']; $this->key = $data['key']; $this->emailID = idx($data, 'emailID'); } public function processRequest() { $request = $this->getRequest(); if ($request->getUser()->isLoggedIn()) { return $this->renderError( pht('You are already logged in.')); } $target_user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($this->id)) ->executeOne(); if (!$target_user) { return new Aphront404Response(); } // NOTE: As a convenience to users, these one-time login URIs may also // be associated with an email address which will be verified when the // URI is used. // This improves the new user experience for users receiving "Welcome" // emails on installs that require verification: if we did not verify the // email, they'd immediately get roadblocked with a "Verify Your Email" // error and have to go back to their email account, wait for a // "Verification" email, and then click that link to actually get access to // their account. This is hugely unwieldy, and if the link was only sent // to the user's email in the first place we can safely verify it as a // side effect of login. // The email hashed into the URI so users can't verify some email they // do not own by doing this: // // - Add some address you do not own; // - request a password reset; // - change the URI in the email to the address you don't own; // - login via the email link; and // - get a "verified" address you don't control. $target_email = null; if ($this->emailID) { $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'userPHID = %s AND id = %d', $target_user->getPHID(), $this->emailID); if (!$target_email) { return new Aphront404Response(); } } $engine = new PhabricatorAuthSessionEngine(); $token = $engine->loadOneTimeLoginKey( $target_user, $target_email, $this->key); if (!$token) { return $this->newDialog() ->setTitle(pht('Unable to Login')) ->setShortTitle(pht('Login Failure')) ->appendParagraph( pht( 'The login link you clicked is invalid, out of date, or has '. 'already been used.')) ->appendParagraph( pht( 'Make sure you are copy-and-pasting the entire link into '. 'your browser. Login links are only valid for 24 hours, and '. 'can only be used once.')) ->appendParagraph( pht('You can try again, or request a new link via email.')) ->addCancelButton('/login/email/', pht('Send Another Email')); } if ($request->isFormPost()) { // If we have an email bound into this URI, verify email so that clicking // the link in the "Welcome" email is good enough, without requiring users // to go through a second round of email verification. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); // Nuke the token so that this URI is one-time only. $token->delete(); if ($target_email) { id(new PhabricatorUserEditor()) ->setActor($target_user) ->verifyEmail($target_user, $target_email); } unset($unguarded); $next = '/'; - if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { + if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { $next = '/settings/panel/external/'; } else if (PhabricatorEnv::getEnvConfig('account.editable')) { // We're going to let the user reset their password without knowing // the old one. Generate a one-time token for that. $key = Filesystem::readRandomCharacters(16); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); id(new PhabricatorAuthTemporaryToken()) ->setObjectPHID($target_user->getPHID()) ->setTokenType( PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE) ->setTokenExpires(time() + phutil_units('1 hour in seconds')) ->setTokenCode(PhabricatorHash::digest($key)) ->save(); unset($unguarded); $next = (string)id(new PhutilURI('/settings/panel/password/')) ->setQueryParams( array( 'key' => $key, )); $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); } PhabricatorCookies::setNextURICookie($request, $next, $force = true); return $this->loginUser($target_user); } // NOTE: We need to CSRF here so attackers can't generate an email link, // then log a user in to an account they control via sneaky invisible // form submissions. switch ($this->linkType) { case PhabricatorAuthSessionEngine::ONETIME_WELCOME: $title = pht('Welcome to Phabricator'); break; case PhabricatorAuthSessionEngine::ONETIME_RECOVER: $title = pht('Account Recovery'); break; case PhabricatorAuthSessionEngine::ONETIME_USERNAME: case PhabricatorAuthSessionEngine::ONETIME_RESET: default: $title = pht('Login to Phabricator'); break; } $body = array(); $body[] = pht( 'Use the button below to log in as: %s', phutil_tag('strong', array(), $target_user->getUsername())); if ($target_email && !$target_email->getIsVerified()) { $body[] = pht( 'Logging in will verify %s as an email address you own.', phutil_tag('strong', array(), $target_email->getAddress())); } $body[] = pht( 'After logging in you should set a password for your account, or '. 'link your account to an external account that you can use to '. 'authenticate in the future.'); $dialog = $this->newDialog() ->setTitle($title) ->addSubmitButton(pht('Login (%s)', $target_user->getUsername())) ->addCancelButton('/'); foreach ($body as $paragraph) { $dialog->appendParagraph($paragraph); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 9085e9309a..a481a5df30 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -1,587 +1,587 @@ accountKey = idx($data, 'akey'); } public function processRequest() { $request = $this->getRequest(); if ($request->getUser()->isLoggedIn()) { return $this->renderError(pht('You are already logged in.')); } $is_setup = false; if (strlen($this->accountKey)) { $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); list($account, $provider, $response) = $result; $is_default = false; } else if ($this->isFirstTimeSetup()) { list($account, $provider, $response) = $this->loadSetupAccount(); $is_default = true; $is_setup = true; } else { list($account, $provider, $response) = $this->loadDefaultAccount(); $is_default = true; } if ($response) { return $response; } if (!$provider->shouldAllowRegistration()) { // TODO: This is a routine error if you click "Login" on an external // auth source which doesn't allow registration. The error should be // more tailored. return $this->renderError( pht( 'The account you are attempting to register with uses an '. 'authentication provider ("%s") which does not allow registration. '. 'An administrator may have recently disabled registration with this '. 'provider.', $provider->getProviderName())); } $user = new PhabricatorUser(); $default_username = $account->getUsername(); $default_realname = $account->getRealName(); $default_email = $account->getEmail(); if (!PhabricatorUserEmail::isValidAddress($default_email)) { $default_email = null; } if ($default_email !== null) { // If the account source provided an email, but it's not allowed by // the configuration, roadblock the user. Previously, we let the user // pick a valid email address instead, but this does not align well with // user expectation and it's not clear the cases it enables are valuable. // See discussion in T3472. if (!PhabricatorUserEmail::isAllowedAddress($default_email)) { return $this->renderError( array( pht( 'The account you are attempting to register with has an invalid '. 'email address (%s). This Phabricator install only allows '. 'registration with specific email addresses:', $default_email), phutil_tag('br'), phutil_tag('br'), PhabricatorUserEmail::describeAllowedAddresses(), )); } // If the account source provided an email, but another account already // has that email, just pretend we didn't get an email. // TODO: See T3340. // TODO: See T3472. if ($default_email !== null) { $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $default_email); if ($same_email) { $default_email = null; } } } $profile = id(new PhabricatorRegistrationProfile()) ->setDefaultUsername($default_username) ->setDefaultEmail($default_email) ->setDefaultRealName($default_realname) ->setCanEditUsername(true) ->setCanEditEmail(($default_email === null)) ->setCanEditRealName(true) ->setShouldVerifyEmail(false); $event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER; $event_data = array( 'account' => $account, 'profile' => $profile, ); $event = id(new PhabricatorEvent($event_type, $event_data)) ->setUser($user); PhutilEventEngine::dispatchEvent($event); $default_username = $profile->getDefaultUsername(); $default_email = $profile->getDefaultEmail(); $default_realname = $profile->getDefaultRealName(); $can_edit_username = $profile->getCanEditUsername(); $can_edit_email = $profile->getCanEditEmail(); $can_edit_realname = $profile->getCanEditRealName(); $must_set_password = $provider->shouldRequireRegistrationPassword(); $can_edit_anything = $profile->getCanEditAnything() || $must_set_password; $force_verify = $profile->getShouldVerifyEmail(); // Automatically verify the administrator's email address during first-time // setup. if ($is_setup) { $force_verify = true; } $value_username = $default_username; $value_realname = $default_realname; $value_email = $default_email; $value_password = null; $errors = array(); $require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name'); $e_username = strlen($value_username) ? null : true; $e_realname = $require_real_name ? true : null; $e_email = strlen($value_email) ? null : true; $e_password = true; $e_captcha = true; $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); $min_len = (int)$min_len; if ($request->isFormPost() || !$can_edit_anything) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); if ($must_set_password) { $e_captcha = pht('Again'); $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); if (!$captcha_ok) { $errors[] = pht('Captcha response is incorrect, try again.'); $e_captcha = pht('Invalid'); } } if ($can_edit_username) { $value_username = $request->getStr('username'); if (!strlen($value_username)) { $e_username = pht('Required'); $errors[] = pht('Username is required.'); } else if (!PhabricatorUser::validateUsername($value_username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } else { $e_username = null; } } if ($must_set_password) { $value_password = $request->getStr('password'); $value_confirm = $request->getStr('confirm'); if (!strlen($value_password)) { $e_password = pht('Required'); $errors[] = pht('You must choose a password.'); } else if ($value_password !== $value_confirm) { $e_password = pht('No Match'); $errors[] = pht('Password and confirmation must match.'); } else if (strlen($value_password) < $min_len) { $e_password = pht('Too Short'); $errors[] = pht( 'Password is too short (must be at least %d characters long).', $min_len); } else if ( PhabricatorCommonPasswords::isCommonPassword($value_password)) { $e_password = pht('Very Weak'); $errors[] = pht( 'Password is pathologically weak. This password is one of the '. 'most common passwords in use, and is extremely easy for '. 'attackers to guess. You must choose a stronger password.'); } else { $e_password = null; } } if ($can_edit_email) { $value_email = $request->getStr('email'); if (!strlen($value_email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isValidAddress($value_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeValidAddresses(); } else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) { $e_email = pht('Disallowed'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } } if ($can_edit_realname) { $value_realname = $request->getStr('realName'); if (!strlen($value_realname) && $require_real_name) { $e_realname = pht('Required'); $errors[] = pht('Real name is required.'); } else { $e_realname = null; } } if (!$errors) { $image = $this->loadProfilePicture($account); if ($image) { $user->setProfileImagePHID($image->getPHID()); } try { if ($force_verify) { $verify_email = true; } else { $verify_email = ($account->getEmailVerified()) && ($value_email === $default_email); } if ($provider->shouldTrustEmails() && $value_email === $default_email) { $verify_email = true; } $email_obj = id(new PhabricatorUserEmail()) ->setAddress($value_email) ->setIsVerified((int)$verify_email); $user->setUsername($value_username); $user->setRealname($value_realname); if ($is_setup) { $must_approve = false; } else { $must_approve = PhabricatorEnv::getEnvConfig( 'auth.require-approval'); } if ($must_approve) { $user->setIsApproved(0); } else { $user->setIsApproved(1); } $user->openTransaction(); $editor = id(new PhabricatorUserEditor()) ->setActor($user); $editor->createNewUser($user, $email_obj); if ($must_set_password) { $envelope = new PhutilOpaqueEnvelope($value_password); $editor->changePassword($user, $envelope); } if ($is_setup) { $editor->makeAdminUser($user, true); } $account->setUserPHID($user->getPHID()); $provider->willRegisterAccount($account); $account->save(); $user->saveTransaction(); if (!$email_obj->getIsVerified()) { $email_obj->sendVerificationEmail($user); } if ($must_approve) { $this->sendWaitingForApprovalEmail($user); } return $this->loginUser($user); } catch (AphrontQueryDuplicateKeyException $exception) { $same_username = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user->getUserName()); $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $value_email); if ($same_username) { $e_username = pht('Duplicate'); $errors[] = pht('Another user already has that username.'); } if ($same_email) { // TODO: See T3340. $e_email = pht('Duplicate'); $errors[] = pht('Another user already has that email.'); } if (!$same_username && !$same_email) { throw $exception; } } } unset($unguarded); } $form = id(new AphrontFormView()) ->setUser($request->getUser()); if (!$is_default) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('External Account')) ->setValue( id(new PhabricatorAuthAccountView()) ->setUser($request->getUser()) ->setExternalAccount($account) ->setAuthProvider($provider))); } if ($can_edit_username) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phabricator Username')) ->setName('username') ->setValue($value_username) ->setError($e_username)); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Phabricator Username')) ->setValue($value_username) ->setError($e_username)); } if ($must_set_password) { $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password') ->setError($e_password) ->setCaption( $min_len ? pht('Minimum length of %d characters.', $min_len) : null)); $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Confirm Password')) ->setName('confirm') ->setError($e_password)); } if ($can_edit_email) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($value_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); } if ($can_edit_realname) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Real Name')) ->setName('realName') ->setValue($value_realname) ->setError($e_realname)); } if ($must_set_password) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); } $submit = id(new AphrontFormSubmitControl()); if ($is_setup) { $submit ->setValue(pht('Create Admin Account')); } else { $submit ->addCancelButton($this->getApplicationURI('start/')) ->setValue(pht('Register Phabricator Account')); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($is_setup) { $crumbs->addTextCrumb(pht('Setup Admin Account')); $title = pht('Welcome to Phabricator'); } else { $crumbs->addTextCrumb(pht('Register')); $crumbs->addTextCrumb($provider->getProviderName()); $title = pht('Phabricator Registration'); } $welcome_view = null; if ($is_setup) { $welcome_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Welcome to Phabricator')) ->appendChild( pht( 'Installation is complete. Register your administrator account '. 'below to log in. You will be able to configure options and add '. 'other authentication mechanisms (like LDAP or OAuth) later on.')); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormErrors($errors); return $this->buildApplicationPage( array( $crumbs, $welcome_view, $object_box, ), array( 'title' => $title, )); } private function loadDefaultAccount() { $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $account = null; $provider = null; $response = null; foreach ($providers as $key => $candidate_provider) { if (!$candidate_provider->shouldAllowRegistration()) { unset($providers[$key]); continue; } if (!$candidate_provider->isDefaultRegistrationProvider()) { unset($providers[$key]); } } if (!$providers) { $response = $this->renderError( pht( 'There are no configured default registration providers.')); return array($account, $provider, $response); } else if (count($providers) > 1) { $response = $this->renderError( pht( 'There are too many configured default registration providers.')); return array($account, $provider, $response); } $provider = head($providers); $account = $provider->getDefaultExternalAccount(); return array($account, $provider, $response); } private function loadSetupAccount() { - $provider = new PhabricatorAuthProviderPassword(); + $provider = new PhabricatorPasswordAuthProvider(); $provider->attachProviderConfig( id(new PhabricatorAuthProviderConfig()) ->setShouldAllowRegistration(1) ->setShouldAllowLogin(1) ->setIsEnabled(true)); $account = $provider->getDefaultExternalAccount(); $response = null; return array($account, $provider, $response); } private function loadProfilePicture(PhabricatorExternalAccount $account) { $phid = $account->getProfileImagePHID(); if (!$phid) { return null; } // NOTE: Use of omnipotent user is okay here because the registering user // can not control the field value, and we can't use their user object to // do meaningful policy checks anyway since they have not registered yet. // Reaching this means the user holds the account secret key and the // registration secret key, and thus has permission to view the image. $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return null; } try { $xformer = new PhabricatorImageTransformer(); return $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } catch (Exception $ex) { phlog($ex); return null; } } protected function renderError($message) { return $this->renderErrorPage( pht('Registration Failed'), array($message)); } private function sendWaitingForApprovalEmail(PhabricatorUser $user) { $title = '[Phabricator] '.pht( 'New User "%s" Awaiting Approval', $user->getUsername()); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection( pht( 'Newly registered user "%s" is awaiting account approval by an '. 'administrator.', $user->getUsername())); $body->addTextSection( pht('APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/people/query/approval/')); $body->addTextSection( pht('DISABLE APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/config/edit/auth.require-approval/')); $admins = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIsAdmin(true) ->execute(); if (!$admins) { return; } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(mpull($admins, 'getPHID')) ->setSubject($title) ->setBody($body->render()) ->saveAndSend(); } } diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 60b006eda2..7ec9ae97b1 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -1,155 +1,155 @@ getRequest(); - if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { + if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { return new Aphront400Response(); } $e_email = true; $e_captcha = true; $errors = array(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($request->isFormPost()) { $e_email = null; $e_captcha = pht('Again'); $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); if (!$captcha_ok) { $errors[] = pht('Captcha response is incorrect, try again.'); $e_captcha = pht('Invalid'); } $email = $request->getStr('email'); if (!strlen($email)) { $errors[] = pht('You must provide an email address.'); $e_email = pht('Required'); } if (!$errors) { // NOTE: Don't validate the email unless the captcha is good; this makes // it expensive to fish for valid email addresses while giving the user // a better error if they goof their email. $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $email); $target_user = null; if ($target_email) { $target_user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $target_email->getUserPHID()); } if (!$target_user) { $errors[] = pht('There is no account associated with that email address.'); $e_email = pht('Invalid'); } if (!$errors) { $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $target_user, null, PhabricatorAuthSessionEngine::ONETIME_RESET); if ($is_serious) { $body = <<setSubject(pht('[Phabricator] Password Reset')) ->addRawTos(array($target_email->getAddress())) ->setBody($body) ->saveAndSend(); return $this->newDialog() ->setTitle(pht('Check Your Email')) ->setShortTitle(pht('Email Sent')) ->appendParagraph( pht('An email has been sent with a link you can use to login.')) ->addCancelButton('/', pht('Done')); } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); } $email_auth = new PHUIFormLayoutView(); $email_auth->appendChild($error_view); $email_auth ->setUser($request->getUser()) ->setFullWidth(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($request->getStr('email')) ->setError($e_email)) ->appendChild( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Reset Password')); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle(pht( 'Forgot Password / Email Login')); $dialog->appendChild($email_auth); $dialog->addSubmitButton(pht('Send Email')); $dialog->setSubmitURI('/login/email/'); return $this->buildApplicationPage( array( $crumbs, $dialog, ), array( 'title' => pht('Forgot Password'), )); } } diff --git a/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php index 85ff0b1e0a..e45d053c60 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php @@ -1,67 +1,67 @@ setName('ldap') ->setExamples('**ldap**') ->setSynopsis( pht('Analyze and diagnose issues with LDAP configuration.')); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $console->getServer()->setEnableLog(true); - $provider = PhabricatorAuthProviderLDAP::getLDAPProvider(); + $provider = PhabricatorLDAPAuthProvider::getLDAPProvider(); if (!$provider) { $console->writeOut( "%s\n", 'The LDAP authentication provider is not enabled.'); exit(1); } if (!function_exists('ldap_connect')) { $console->writeOut( "%s\n", 'The LDAP extension is not enabled.'); exit(1); } $adapter = $provider->getAdapter(); $console->writeOut("%s\n", pht('Enter LDAP Credentials')); $username = phutil_console_prompt('LDAP Username: '); if (!strlen($username)) { throw new PhutilArgumentUsageException( pht('You must enter an LDAP username.')); } phutil_passthru('stty -echo'); $password = phutil_console_prompt('LDAP Password: '); phutil_passthru('stty echo'); if (!strlen($password)) { throw new PhutilArgumentUsageException( pht('You must enter an LDAP password.')); } $adapter->setLoginUsername($username); $adapter->setLoginPassword(new PhutilOpaqueEnvelope($password)); $console->writeOut("\n"); $console->writeOut("%s\n", pht('Connecting to LDAP...')); $account_id = $adapter->getAccountID(); if ($account_id) { $console->writeOut("%s\n", pht('Found LDAP Account: %s', $account_id)); } else { $console->writeOut("%s\n", pht('Unable to find LDAP account!')); } return 0; } } diff --git a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php index 27382eb48b..b05b87f5e8 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php @@ -1,156 +1,156 @@ setName('refresh') ->setExamples('**refresh**') ->setSynopsis( pht( 'Refresh OAuth access tokens. This is primarily useful for '. 'development and debugging.')) ->setArguments( array( array( 'name' => 'user', 'param' => 'user', 'help' => 'Refresh tokens for a given user.', ), array( 'name' => 'type', 'param' => 'provider', 'help' => 'Refresh tokens for a given provider type.', ), array( 'name' => 'domain', 'param' => 'domain', 'help' => 'Refresh tokens for a given domain.', ), )); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $viewer = $this->getViewer(); $query = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )); $username = $args->getArg('user'); if (strlen($username)) { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withUsernames(array($username)) ->executeOne(); if ($user) { $query->withUserPHIDs(array($user->getPHID())); } else { throw new PhutilArgumentUsageException( pht('No such user "%s"!', $username)); } } $type = $args->getArg('type'); if (strlen($type)) { $query->withAccountTypes(array($type)); } $domain = $args->getArg('domain'); if (strlen($domain)) { $query->withAccountDomains(array($domain)); } $accounts = $query->execute(); if (!$accounts) { throw new PhutilArgumentUsageException( pht('No accounts match the arguments!')); } else { $console->writeOut( "%s\n", pht( 'Found %s account(s) to refresh.', new PhutilNumber(count($accounts)))); } $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($accounts as $account) { $console->writeOut( "%s\n", pht( 'Refreshing account #%d (%s/%s).', $account->getID(), $account->getAccountType(), $account->getAccountDomain())); $key = $account->getProviderKey(); if (empty($providers[$key])) { $console->writeOut( "> %s\n", pht('Skipping, provider is not enabled or does not exist.')); continue; } $provider = $providers[$key]; - if (!($provider instanceof PhabricatorAuthProviderOAuth2)) { + if (!($provider instanceof PhabricatorOAuth2AuthProvider)) { $console->writeOut( "> %s\n", pht('Skipping, provider is not an OAuth2 provider.')); continue; } $adapter = $provider->getAdapter(); if (!$adapter->supportsTokenRefresh()) { $console->writeOut( "> %s\n", pht('Skipping, provider does not support token refresh.')); continue; } $refresh_token = $account->getProperty('oauth.token.refresh'); if (!$refresh_token) { $console->writeOut( "> %s\n", pht('Skipping, provider has no stored refresh token.')); continue; } $console->writeOut( "+ %s\n", pht( 'Refreshing token, current token expires in %s seconds.', new PhutilNumber( $account->getProperty('oauth.token.access.expires') - time()))); $token = $provider->getOAuthAccessToken($account, $force_refresh = true); if (!$token) { $console->writeOut( "* %s\n", pht('Unable to refresh token!')); continue; } $console->writeOut( "+ %s\n", pht( 'Refreshed token, new token expires in %s seconds.', new PhutilNumber( $account->getProperty('oauth.token.access.expires') - time()))); } $console->writeOut("%s\n", pht('Done.')); return 0; } } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index bc381ad636..380ac89a8e 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -1,492 +1,492 @@ providerConfig = $config; return $this; } public function hasProviderConfig() { return (bool)$this->providerConfig; } public function getProviderConfig() { if ($this->providerConfig === null) { throw new Exception( 'Call attachProviderConfig() before getProviderConfig()!'); } return $this->providerConfig; } public function getConfigurationHelp() { return null; } public function getDefaultProviderConfig() { return id(new PhabricatorAuthProviderConfig()) ->setProviderClass(get_class($this)) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration(1) ->setShouldAllowLink(1) ->setShouldAllowUnlink(1); } public function getNameForCreate() { return $this->getProviderName(); } public function getDescriptionForCreate() { return null; } public function getProviderKey() { return $this->getAdapter()->getAdapterKey(); } public function getProviderType() { return $this->getAdapter()->getAdapterType(); } public function getProviderDomain() { return $this->getAdapter()->getAdapterDomain(); } public static function getAllBaseProviders() { static $providers; if ($providers === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $providers = $objects; } return $providers; } public static function getAllProviders() { static $providers; if ($providers === null) { $objects = self::getAllBaseProviders(); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->execute(); $providers = array(); foreach ($configs as $config) { if (!isset($objects[$config->getProviderClass()])) { // This configuration is for a provider which is not installed. continue; } $object = clone $objects[$config->getProviderClass()]; $object->attachProviderConfig($config); $key = $object->getProviderKey(); if (isset($providers[$key])) { throw new Exception( pht( "Two authentication providers use the same provider key ". "('%s'). Each provider must be identified by a unique ". "key.", $key)); } $providers[$key] = $object; } } return $providers; } public static function getAllEnabledProviders() { $providers = self::getAllProviders(); foreach ($providers as $key => $provider) { if (!$provider->isEnabled()) { unset($providers[$key]); } } return $providers; } public static function getEnabledProviderByKey($provider_key) { return idx(self::getAllEnabledProviders(), $provider_key); } abstract public function getProviderName(); abstract public function getAdapter(); public function isEnabled() { return $this->getProviderConfig()->getIsEnabled(); } public function shouldAllowLogin() { return $this->getProviderConfig()->getShouldAllowLogin(); } public function shouldAllowRegistration() { return $this->getProviderConfig()->getShouldAllowRegistration(); } public function shouldAllowAccountLink() { return $this->getProviderConfig()->getShouldAllowLink(); } public function shouldAllowAccountUnlink() { return $this->getProviderConfig()->getShouldAllowUnlink(); } public function shouldTrustEmails() { return $this->shouldAllowEmailTrustConfiguration() && $this->getProviderConfig()->getShouldTrustEmails(); } /** * Should we allow the adapter to be marked as "trusted" * This is true for all adapters except those that allow the user to type in - * emails (@see PhabricatorAuthProviderPassword) + * emails (@see PhabricatorPasswordAuthProvider) */ public function shouldAllowEmailTrustConfiguration() { return true; } public function buildLoginForm( PhabricatorAuthStartController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'start'); } abstract public function processLoginRequest( PhabricatorAuthLoginController $controller); public function buildLinkForm( PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'link'); } public function shouldAllowAccountRefresh() { return true; } public function buildRefreshForm( PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh'); } protected function renderLoginForm( AphrontRequest $request, $mode) { throw new PhutilMethodNotImplementedException(); } public function createProviders() { return array($this); } protected function willSaveAccount(PhabricatorExternalAccount $account) { return; } public function willRegisterAccount(PhabricatorExternalAccount $account) { return; } protected function loadOrCreateAccount($account_id) { if (!strlen($account_id)) { throw new Exception( 'loadOrCreateAccount(...): empty account ID!'); } $adapter = $this->getAdapter(); $adapter_class = get_class($adapter); if (!strlen($adapter->getAdapterType())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter type."); } if (!strlen($adapter->getAdapterDomain())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter domain."); } $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s', $adapter->getAdapterType(), $adapter->getAdapterDomain(), $account_id); if (!$account) { $account = id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()) ->setAccountID($account_id); } $account->setUsername($adapter->getAccountName()); $account->setRealName($adapter->getAccountRealName()); $account->setEmail($adapter->getAccountEmail()); $account->setAccountURI($adapter->getAccountURI()); $account->setProfileImagePHID(null); $image_uri = $adapter->getAccountImageURI(); if ($image_uri) { try { $name = PhabricatorSlug::normalize($this->getProviderName()); $name = $name.'-profile.jpg'; // TODO: If the image has not changed, we do not need to make a new // file entry for it, but there's no convenient way to do this with // PhabricatorFile right now. The storage will get shared, so the impact // here is negligible. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $image_file = PhabricatorFile::newFromFileDownload( $image_uri, array( 'name' => $name, )); unset($unguarded); if ($image_file) { $account->setProfileImagePHID($image_file->getPHID()); } } catch (Exception $ex) { // Log this but proceed, it's not especially important that we // be able to pull profile images. phlog($ex); } } $this->willSaveAccount($account); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); return $account; } public function getLoginURI() { $app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth'); return $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); } public function getSettingsURI() { return '/settings/panel/external/'; } public function getStartURI() { $app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth'); $uri = $app->getApplicationURI('/start/'); return $uri; } public function isDefaultRegistrationProvider() { return false; } public function shouldRequireRegistrationPassword() { return false; } public function getDefaultExternalAccount() { throw new PhutilMethodNotImplementedException(); } public function getLoginOrder() { return '500-'.$this->getProviderName(); } protected function getLoginIcon() { return 'Generic'; } public function isLoginFormAButton() { return false; } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { return null; } public function readFormValuesFromProvider() { return array(); } public function readFormValuesFromRequest(AphrontRequest $request) { return array(); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { return; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { $account_view = id(new PhabricatorAuthAccountView()) ->setExternalAccount($account) ->setAuthProvider($this); $item->appendChild( phutil_tag( 'div', array( 'class' => 'mmr mml mst mmb', ), $account_view)); } /** * Return true to use a two-step configuration (setup, configure) instead of * the default single-step configuration. In practice, this means that * creating a new provider instance will redirect back to the edit page * instead of the provider list. * * @return bool True if this provider uses two-step configuration. */ public function hasSetupStep() { return false; } /** * Render a standard login/register button element. * * The `$attributes` parameter takes these keys: * * - `uri`: URI the button should take the user to when clicked. * - `method`: Optional HTTP method the button should use, defaults to GET. * * @param AphrontRequest HTTP request. * @param string Request mode string. * @param map Additional parameters, see above. * @return wild Login button. */ protected function renderStandardLoginButton( AphrontRequest $request, $mode, array $attributes = array()) { PhutilTypeSpec::checkMap( $attributes, array( 'method' => 'optional string', 'uri' => 'string', 'sigil' => 'optional string', )); $viewer = $request->getUser(); $adapter = $this->getAdapter(); if ($mode == 'link') { $button_text = pht('Link External Account'); } else if ($mode == 'refresh') { $button_text = pht('Refresh Account Link'); } else if ($this->shouldAllowRegistration()) { $button_text = pht('Login or Register'); } else { $button_text = pht('Login'); } $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($this->getLoginIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($button_text) ->setSubtext($this->getProviderName()); $uri = $attributes['uri']; $uri = new PhutilURI($uri); $params = $uri->getQueryParams(); $uri->setQueryParams(array()); $content = array($button); foreach ($params as $key => $value) { $content[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return phabricator_form( $viewer, array( 'method' => idx($attributes, 'method', 'GET'), 'action' => (string)$uri, 'sigil' => idx($attributes, 'sigil'), ), $content); } public function renderConfigurationFooter() { return null; } protected function getAuthCSRFCode(AphrontRequest $request) { $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID); if (!strlen($phcid)) { throw new Exception( pht( 'Your browser did not submit a "%s" cookie with client state '. 'information in the request. Check that cookies are enabled. '. 'If this problem persists, you may need to clear your cookies.', PhabricatorCookies::COOKIE_CLIENTID)); } return PhabricatorHash::digest($phcid); } protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) { $expect = $this->getAuthCSRFCode($request); if (!strlen($actual)) { throw new Exception( pht( 'The authentication provider did not return a client state '. 'parameter in its response, but one was expected. If this '. 'problem persists, you may need to clear your cookies.')); } if ($actual !== $expect) { throw new Exception( pht( 'The authentication provider did not return the correct client '. 'state parameter in its response. If this problem persists, you may '. 'need to clear your cookies.')); } } } diff --git a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php index e0f4a44ac0..c81cbe64aa 100644 --- a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php @@ -1,289 +1,289 @@ getProviderConfig()->getProperty(self::PROPERTY_JIRA_URI); } public function getProviderName() { return pht('JIRA'); } public function getDescriptionForCreate() { return pht('Configure JIRA OAuth. NOTE: Only supports JIRA 6.'); } public function getConfigurationHelp() { return $this->getProviderConfigurationHelp(); } protected function getProviderConfigurationHelp() { if ($this->isSetup()) { return pht( "**Step 1 of 2**: Provide the name and URI for your JIRA install.\n\n". "In the next step, you will configure JIRA."); } else { $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); return pht( "**Step 2 of 2**: In this step, you will configure JIRA.\n\n". "**Create a JIRA Application**: Log into JIRA and go to ". "**Administration**, then **Add-ons**, then **Application Links**. ". "Click the button labeled **Add Application Link**, and use these ". "settings to create an application:\n\n". " - **Server URL**: `%s`\n". " - Then, click **Next**. On the second page:\n". " - **Application Name**: `Phabricator`\n". " - **Application Type**: `Generic Application`\n". " - Then, click **Create**.\n\n". "**Configure Your Application**: Find the application you just ". "created in the table, and click the **Configure** link under ". "**Actions**. Select **Incoming Authentication** and click the ". "**OAuth** tab (it may be selected by default). Then, use these ". "settings:\n\n". " - **Consumer Key**: Set this to the \"Consumer Key\" value in the ". "form above.\n". " - **Consumer Name**: `Phabricator`\n". " - **Public Key**: Set this to the \"Public Key\" value in the ". "form above.\n". " - **Consumer Callback URL**: `%s`\n". "Click **Save** in JIRA. Authentication should now be configured, ". "and this provider should work correctly.", PhabricatorEnv::getProductionURI('/'), $login_uri); } } protected function newOAuthAdapter() { $config = $this->getProviderConfig(); return id(new PhutilJIRAAuthAdapter()) ->setAdapterDomain($config->getProviderDomain()) ->setJIRABaseURI($config->getProperty(self::PROPERTY_JIRA_URI)) ->setPrivateKey( new PhutilOpaqueEnvelope( $config->getProperty(self::PROPERTY_PRIVATE_KEY))); } protected function getLoginIcon() { return 'Jira'; } private function isSetup() { return !$this->getProviderConfig()->getID(); } const PROPERTY_JIRA_NAME = 'oauth1:jira:name'; const PROPERTY_JIRA_URI = 'oauth1:jira:uri'; const PROPERTY_PUBLIC_KEY = 'oauth1:jira:key:public'; const PROPERTY_PRIVATE_KEY = 'oauth1:jira:key:private'; public function readFormValuesFromProvider() { $config = $this->getProviderConfig(); $uri = $config->getProperty(self::PROPERTY_JIRA_URI); return array( self::PROPERTY_JIRA_NAME => $this->getProviderDomain(), self::PROPERTY_JIRA_URI => $uri, ); } public function readFormValuesFromRequest(AphrontRequest $request) { $is_setup = $this->isSetup(); if ($is_setup) { $name = $request->getStr(self::PROPERTY_JIRA_NAME); } else { $name = $this->getProviderDomain(); } return array( self::PROPERTY_JIRA_NAME => $name, self::PROPERTY_JIRA_URI => $request->getStr(self::PROPERTY_JIRA_URI), ); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); $is_setup = $this->isSetup(); $key_name = self::PROPERTY_JIRA_NAME; $key_uri = self::PROPERTY_JIRA_URI; if (!strlen($values[$key_name])) { $errors[] = pht('JIRA instance name is required.'); $issues[$key_name] = pht('Required'); } else if (!preg_match('/^[a-z0-9.]+\z/', $values[$key_name])) { $errors[] = pht( 'JIRA instance name must contain only lowercase letters, digits, and '. 'period.'); $issues[$key_name] = pht('Invalid'); } if (!strlen($values[$key_uri])) { $errors[] = pht('JIRA base URI is required.'); $issues[$key_uri] = pht('Required'); } else { $uri = new PhutilURI($values[$key_uri]); if (!$uri->getProtocol()) { $errors[] = pht( 'JIRA base URI should include protocol (like "https://").'); $issues[$key_uri] = pht('Invalid'); } } if (!$errors && $is_setup) { $config = $this->getProviderConfig(); $config->setProviderDomain($values[$key_name]); $consumer_key = 'phjira.'.Filesystem::readRandomCharacters(16); list($public, $private) = PhutilJIRAAuthAdapter::newJIRAKeypair(); $config->setProperty(self::PROPERTY_PUBLIC_KEY, $public); $config->setProperty(self::PROPERTY_PRIVATE_KEY, $private); $config->setProperty(self::PROPERTY_CONSUMER_KEY, $consumer_key); } return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { if (!function_exists('openssl_pkey_new')) { // TODO: This could be a bit prettier. throw new Exception( pht( "The PHP 'openssl' extension is not installed. You must install ". "this extension in order to add a JIRA authentication provider, ". "because JIRA OAuth requests use the RSA-SHA1 signing algorithm. ". "Install the 'openssl' extension, restart your webserver, and try ". "again.")); } $form->appendRemarkupInstructions( pht( 'NOTE: This provider **only supports JIRA 6**. It will not work with '. 'JIRA 5 or earlier.')); $is_setup = $this->isSetup(); $e_required = $request->isFormPost() ? null : true; $v_name = $values[self::PROPERTY_JIRA_NAME]; if ($is_setup) { $e_name = idx($issues, self::PROPERTY_JIRA_NAME, $e_required); } else { $e_name = null; } $v_uri = $values[self::PROPERTY_JIRA_URI]; $e_uri = idx($issues, self::PROPERTY_JIRA_URI, $e_required); if ($is_setup) { $form ->appendRemarkupInstructions( pht( "**JIRA Instance Name**\n\n". "Choose a permanent name for this instance of JIRA. Phabricator ". "uses this name internally to keep track of this instance of ". "JIRA, in case the URL changes later.\n\n". "Use lowercase letters, digits, and period. For example, ". "`jira`, `jira.mycompany` or `jira.engineering` are reasonable ". "names.")) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('JIRA Instance Name')) ->setValue($v_name) ->setName(self::PROPERTY_JIRA_NAME) ->setError($e_name)); } else { $form ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('JIRA Instance Name')) ->setValue($v_name)); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('JIRA Base URI')) ->setValue($v_uri) ->setName(self::PROPERTY_JIRA_URI) ->setCaption( pht( 'The URI where JIRA is installed. For example: %s', phutil_tag('tt', array(), 'https://jira.mycompany.com/'))) ->setError($e_uri)); if (!$is_setup) { $config = $this->getProviderConfig(); $ckey = $config->getProperty(self::PROPERTY_CONSUMER_KEY); $ckey = phutil_tag('tt', array(), $ckey); $pkey = $config->getProperty(self::PROPERTY_PUBLIC_KEY); $pkey = phutil_escape_html_newlines($pkey); $pkey = phutil_tag('tt', array(), $pkey); $form ->appendRemarkupInstructions( pht( 'NOTE: **To complete setup**, copy and paste these keys into JIRA '. 'according to the instructions below.')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Consumer Key')) ->setValue($ckey)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Public Key')) ->setValue($pkey)); } } /** * JIRA uses a setup step to generate public/private keys. */ public function hasSetupStep() { return true; } public static function getJIRAProvider() { $providers = self::getAllEnabledProviders(); foreach ($providers as $provider) { - if ($provider instanceof PhabricatorAuthProviderOAuth1JIRA) { + if ($provider instanceof PhabricatorJIRAAuthProvider) { return $provider; } } return null; } public function newJIRAFuture( PhabricatorExternalAccount $account, $path, $method, $params = array()) { $adapter = clone $this->getAdapter(); $adapter->setToken($account->getProperty('oauth1.token')); $adapter->setTokenSecret($account->getProperty('oauth1.token.secret')); return $adapter->newJIRAFuture($path, $method, $params); } } diff --git a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php index 5777bfcae8..f5699295a7 100644 --- a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php @@ -1,476 +1,476 @@ 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('Login or Register with LDAP')); $dialog->addSubmitButton(pht('Login or Register')); } else { $dialog->setTitle(pht('Login with LDAP')); $dialog->addSubmitButton(pht('Login')); } 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('LDAP Username') ->setName('ldap_username') ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel('LDAP Password') ->setName('ldap_password') ->setError($e_pass)); if ($errors) { $errors = id(new AphrontErrorView())->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('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'; 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 function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { $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 login 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 login 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 login ". "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) ->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 PhabricatorAuthProviderLDAP) { + 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 85eb451a21..84e53dd501 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -1,355 +1,355 @@ '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) ->appendChild($table); } public function getDescriptionForCreate() { return pht( 'Allow users to login or register using a username and password.'); } public function getAdapter() { if (!$this->adapter) { - $adapter = new PhutilAuthAdapterEmpty(); + $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 buildLinkForm( PhabricatorAuthLinkController $controller) { throw new Exception("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('Login to Phabricator')) ->addSubmitButton(pht('Login')); 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 AphrontErrorView())->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->setFullWidth(true) ->appendChild($errors) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username or Email') ->setName('username') ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel('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(); $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( PhabricatorUserLog::ACTION_LOGIN_FAILURE, 60 * 15); if (count($failed_attempts) > 5) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } $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')); if ($user->comparePassword($envelope)) { $account = $this->loadOrCreateAccount($user->getPHID()); $log_user = $user; // If the user's password is stored using a less-than-optimal // hash, upgrade them to the strongest available hash. $hash_envelope = new PhutilOpaqueEnvelope( $user->getPasswordHash()); if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { $user->setPassword($envelope); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $user->save(); unset($unguarded); } } } } } } 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 PhabricatorAuthProviderPassword) { + 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/applications/differential/customfield/DifferentialJIRAIssuesField.php b/src/applications/differential/customfield/DifferentialJIRAIssuesField.php index 3d0c6d61ac..8c6f66bb20 100644 --- a/src/applications/differential/customfield/DifferentialJIRAIssuesField.php +++ b/src/applications/differential/customfield/DifferentialJIRAIssuesField.php @@ -1,313 +1,313 @@ getValue()); } public function setValueFromStorage($value) { try { $this->setValue(phutil_json_decode($value)); } catch (PhutilJSONParserException $ex) { $this->setValue(array()); } return $this; } public function getFieldName() { return pht('JIRA Issues'); } public function getFieldDescription() { return pht('Lists associated JIRA issues.'); } public function shouldAppearInPropertyView() { return true; } public function renderPropertyViewLabel() { return $this->getFieldName(); } public function renderPropertyViewValue(array $handles) { $xobjs = $this->loadDoorkeeperExternalObjects($this->getValue()); if (!$xobjs) { return null; } $links = array(); foreach ($xobjs as $xobj) { $links[] = id(new DoorkeeperTagView()) ->setExternalObject($xobj); } return phutil_implode_html(phutil_tag('br'), $links); } private function buildDoorkeeperRefs($value) { - $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); + $provider = PhabricatorJIRAAuthProvider::getJIRAProvider(); $refs = array(); if ($value) { foreach ($value as $jira_key) { $refs[] = id(new DoorkeeperObjectRef()) ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA) ->setApplicationDomain($provider->getProviderDomain()) ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE) ->setObjectID($jira_key); } } return $refs; } private function loadDoorkeeperExternalObjects($value) { $refs = $this->buildDoorkeeperRefs($value); if (!$refs) { return array(); } $xobjs = id(new DoorkeeperExternalObjectQuery()) ->setViewer($this->getViewer()) ->withObjectKeys(mpull($refs, 'getObjectKey')) ->execute(); return $xobjs; } public function shouldAppearInEditView() { - return PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); + return PhabricatorJIRAAuthProvider::getJIRAProvider(); } public function shouldAppearInApplicationTransactions() { - return PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); + return PhabricatorJIRAAuthProvider::getJIRAProvider(); } public function readValueFromRequest(AphrontRequest $request) { $this->setValue($request->getStrList($this->getFieldKey())); return $this; } public function renderEditControl(array $handles) { return id(new AphrontFormTextControl()) ->setLabel(pht('JIRA Issues')) ->setCaption( pht('Example: %s', phutil_tag('tt', array(), 'JIS-3, JIS-9'))) ->setName($this->getFieldKey()) ->setValue(implode(', ', nonempty($this->getValue(), array()))) ->setError($this->error); } public function getOldValueForApplicationTransactions() { return array_unique(nonempty($this->getValue(), array())); } public function getNewValueForApplicationTransactions() { return array_unique(nonempty($this->getValue(), array())); } public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { $this->error = null; $errors = parent::validateApplicationTransactions( $editor, $type, $xactions); $transaction = null; foreach ($xactions as $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_diff($new, $old); if (!$add) { continue; } // Only check that the actor can see newly added JIRA refs. You're // allowed to remove refs or make no-op changes even if you aren't // linked to JIRA. try { $refs = id(new DoorkeeperImportEngine()) ->setViewer($this->getViewer()) ->setRefs($this->buildDoorkeeperRefs($add)) ->setThrowOnMissingLink(true) ->execute(); } catch (DoorkeeperMissingLinkException $ex) { $this->error = pht('Not Linked'); $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Not Linked'), pht( 'You can not add JIRA issues (%s) to this revision because your '. 'Phabricator account is not linked to a JIRA account.', implode(', ', $add)), $xaction); continue; } $bad = array(); foreach ($refs as $ref) { if (!$ref->getIsVisible()) { $bad[] = $ref->getObjectID(); } } if ($bad) { $bad = implode(', ', $bad); $this->error = pht('Invalid'); $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'Some JIRA issues could not be loaded. They may not exist, or '. 'you may not have permission to view them: %s', $bad), $xaction); } } return $errors; } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); if (!is_array($old)) { $old = array(); } $new = $xaction->getNewValue(); if (!is_array($new)) { $new = array(); } $add = array_diff($new, $old); $rem = array_diff($old, $new); $author_phid = $xaction->getAuthorPHID(); if ($add && $rem) { return pht( '%s updated JIRA issue(s): added %d %s; removed %d %s.', $xaction->renderHandleLink($author_phid), new PhutilNumber(count($add)), implode(', ', $add), new PhutilNumber(count($rem)), implode(', ', $rem)); } else if ($add) { return pht( '%s added %d JIRA issue(s): %s.', $xaction->renderHandleLink($author_phid), new PhutilNumber(count($add)), implode(', ', $add)); } else if ($rem) { return pht( '%s removed %d JIRA issue(s): %s.', $xaction->renderHandleLink($author_phid), new PhutilNumber(count($rem)), implode(', ', $rem)); } return parent::getApplicationTransactionTitle($xaction); } public function applyApplicationTransactionExternalEffects( PhabricatorApplicationTransaction $xaction) { // Update the CustomField storage. parent::applyApplicationTransactionExternalEffects($xaction); // Now, synchronize the Doorkeeper edges. $revision = $this->getObject(); $revision_phid = $revision->getPHID(); $edge_type = PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE; $xobjs = $this->loadDoorkeeperExternalObjects($xaction->getNewValue()); $edge_dsts = mpull($xobjs, 'getPHID'); $edges = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision_phid, $edge_type); $editor = new PhabricatorEdgeEditor(); foreach (array_diff($edges, $edge_dsts) as $rem_edge) { $editor->removeEdge($revision_phid, $edge_type, $rem_edge); } foreach (array_diff($edge_dsts, $edges) as $add_edge) { $editor->addEdge($revision_phid, $edge_type, $add_edge); } $editor->save(); } public function shouldAppearInCommitMessage() { return true; } public function shouldAppearInCommitMessageTemplate() { return true; } public function getCommitMessageLabels() { return array( 'JIRA', 'JIRA Issues', 'JIRA Issue', ); } public function parseValueFromCommitMessage($value) { return preg_split('/[\s,]+/', $value, $limit = -1, PREG_SPLIT_NO_EMPTY); } public function readValueFromCommitMessage($value) { $this->setValue($value); return $this; } public function renderCommitMessageValue(array $handles) { $value = $this->getValue(); if (!$value) { return null; } return implode(', ', $value); } public function shouldAppearInConduitDictionary() { return true; } } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php index 19191eb03c..79e711e00a 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php @@ -1,129 +1,129 @@ getApplicationType() != self::APPTYPE_ASANA) { return false; } if ($ref->getApplicationDomain() != self::APPDOMAIN_ASANA) { return false; } $types = array( self::OBJTYPE_TASK => true, ); return isset($types[$ref->getObjectType()]); } public function pullRefs(array $refs) { $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); $viewer = $this->getViewer(); - $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); + $provider = PhabricatorAsanaAuthProvider::getAsanaProvider(); if (!$provider) { return; } $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); if (!$accounts) { return $this->didFailOnMissingLink(); } // TODO: If the user has several linked Asana accounts, we just pick the // first one arbitrarily. We might want to try using all of them or do // something with more finesse. There's no UI way to link multiple accounts // right now so this is currently moot. $account = head($accounts); $token = $provider->getOAuthAccessToken($account); if (!$token) { return; } $template = id(new PhutilAsanaFuture()) ->setAccessToken($token); $futures = array(); foreach ($id_map as $key => $id) { $futures[$key] = id(clone $template) ->setRawAsanaQuery("tasks/{$id}"); } $results = array(); $failed = array(); foreach (Futures($futures) as $key => $future) { try { $results[$key] = $future->resolve(); } catch (Exception $ex) { if (($ex instanceof HTTPFutureResponseStatus) && ($ex->getStatusCode() == 404)) { // This indicates that the object has been deleted (or never existed, // or isn't visible to the current user) but it's a successful sync of // an object which isn't visible. } else { // This is something else, so consider it a synchronization failure. phlog($ex); $failed[$key] = $ex; } } } foreach ($refs as $ref) { $ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID())); $did_fail = idx($failed, $ref->getObjectKey()); if ($did_fail) { $ref->setSyncFailed(true); continue; } $result = idx($results, $ref->getObjectKey()); if (!$result) { continue; } $ref->setIsVisible(true); $ref->setAttribute('asana.data', $result); $ref->setAttribute('fullname', pht('Asana: %s', $result['name'])); $ref->setAttribute('title', $result['name']); $ref->setAttribute('description', $result['notes']); $obj = $ref->getExternalObject(); if ($obj->getID()) { continue; } $this->fillObjectFromData($obj, $result); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $obj->save(); unset($unguarded); } } public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { $id = $result['id']; $uri = "https://app.asana.com/0/{$id}/{$id}"; $obj->setObjectURI($uri); } } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php index 9698237f1d..6349943770 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php @@ -1,148 +1,148 @@ getApplicationType() != self::APPTYPE_JIRA) { return false; } $types = array( self::OBJTYPE_ISSUE => true, ); return isset($types[$ref->getObjectType()]); } public function pullRefs(array $refs) { $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); $viewer = $this->getViewer(); - $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); + $provider = PhabricatorJIRAAuthProvider::getJIRAProvider(); if (!$provider) { return; } $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); if (!$accounts) { return $this->didFailOnMissingLink(); } // TODO: When we support multiple JIRA instances, we need to disambiguate // issues (perhaps with additional configuration) or cast a wide net // (by querying all instances). For now, just query the one instance. $account = head($accounts); $futures = array(); foreach ($id_map as $key => $id) { $futures[$key] = $provider->newJIRAFuture( $account, 'rest/api/2/issue/'.phutil_escape_uri($id), 'GET'); } $results = array(); $failed = array(); foreach (Futures($futures) as $key => $future) { try { $results[$key] = $future->resolveJSON(); } catch (Exception $ex) { if (($ex instanceof HTTPFutureResponseStatus) && ($ex->getStatusCode() == 404)) { // This indicates that the object has been deleted (or never existed, // or isn't visible to the current user) but it's a successful sync of // an object which isn't visible. } else { // This is something else, so consider it a synchronization failure. phlog($ex); $failed[$key] = $ex; } } } foreach ($refs as $ref) { $ref->setAttribute('name', pht('JIRA %s', $ref->getObjectID())); $did_fail = idx($failed, $ref->getObjectKey()); if ($did_fail) { $ref->setSyncFailed(true); continue; } $result = idx($results, $ref->getObjectKey()); if (!$result) { continue; } $fields = idx($result, 'fields', array()); $ref->setIsVisible(true); $ref->setAttribute( 'fullname', pht('JIRA %s %s', $result['key'], idx($fields, 'summary'))); $ref->setAttribute('title', idx($fields, 'summary')); $ref->setAttribute('description', idx($result, 'description')); $obj = $ref->getExternalObject(); if ($obj->getID()) { continue; } $this->fillObjectFromData($obj, $result); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $obj->save(); unset($unguarded); } } public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { // Convert the "self" URI, which points at the REST endpoint, into a // browse URI. $self = idx($result, 'self'); $object_id = $obj->getObjectID(); $uri = self::getJIRAIssueBrowseURIFromJIRARestURI($self, $object_id); if ($uri !== null) { $obj->setObjectURI($uri); } } public static function getJIRAIssueBrowseURIFromJIRARestURI( $uri, $object_id) { $uri = new PhutilURI($uri); // The JIRA install might not be at the domain root, so we may need to // keep an initial part of the path, like "/jira/". Find the API specific // part of the URI, strip it off, then replace it with the web version. $path = $uri->getPath(); $pos = strrpos($path, 'rest/api/2/issue/'); if ($pos === false) { return null; } $path = substr($path, 0, $pos); $path = $path.'browse/'.$object_id; $uri->setPath($path); return (string)$uri; } } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index ca452a5ff8..800d18a287 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -1,153 +1,153 @@ newOption('asana.workspace-id', 'string', null) ->setSummary(pht('Asana Workspace ID to publish into.')) ->setDescription( pht( 'To enable synchronization into Asana, enter an Asana Workspace '. 'ID here.'. "\n\n". "NOTE: This feature is new and experimental.")), $this->newOption('asana.project-ids', 'wild', null) ->setSummary(pht('Optional Asana projects to use as application tags.')) ->setDescription( pht( 'When Phabricator creates tasks in Asana, it can add the tasks '. 'to Asana projects based on which application the corresponding '. 'object in Phabricator comes from. For example, you can add code '. 'reviews in Asana to a "Differential" project.'. "\n\n". 'NOTE: This feature is new and experimental.')) ); } public function renderContextualDescription( PhabricatorConfigOption $option, AphrontRequest $request) { switch ($option->getKey()) { case 'asana.workspace-id': break; case 'asana.project-ids': return $this->renderContextualProjectDescription($option, $request); default: return parent::renderContextualDescription($option, $request); } $viewer = $request->getUser(); - $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); + $provider = PhabricatorAsanaAuthProvider::getAsanaProvider(); if (!$provider) { return null; } $account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return null; } $token = $provider->getOAuthAccessToken($account); if (!$token) { return null; } try { $workspaces = id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setRawAsanaQuery('workspaces') ->resolve(); } catch (Exception $ex) { return null; } if (!$workspaces) { return null; } $out = array(); $out[] = pht('| Workspace ID | Workspace Name |'); $out[] = '| ------------ | -------------- |'; foreach ($workspaces as $workspace) { $out[] = sprintf('| `%s` | `%s` |', $workspace['id'], $workspace['name']); } $out = implode("\n", $out); $out = pht( "The Asana Workspaces your linked account has access to are:\n\n%s", $out); return PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($out), 'default', $viewer); } private function renderContextualProjectDescription( PhabricatorConfigOption $option, AphrontRequest $request) { $viewer = $request->getUser(); $publishers = id(new PhutilSymbolLoader()) ->setAncestorClass('DoorkeeperFeedStoryPublisher') ->loadObjects(); $out = array(); $out[] = pht( 'To specify projects to add tasks to, enter a JSON map with publisher '. 'class names as keys and a list of project IDs as values. For example, '. 'to put Differential tasks into Asana projects with IDs `123` and '. '`456`, enter:'. "\n\n". " lang=txt\n". " {\n". " \"DifferentialDoorkeeperRevisionFeedStoryPublisher\" : [123, 456]\n". " }\n"); $out[] = pht('Available publishers class names are:'); foreach ($publishers as $publisher) { $out[] = ' - `'.get_class($publisher).'`'; } $out[] = pht( 'You can find an Asana project ID by clicking the project in Asana and '. 'then examining the URL:'. "\n\n". " lang=txt\n". " https://app.asana.com/0/12345678901234567890/111111111111111111\n". " ^^^^^^^^^^^^^^^^^^^^\n". " This is the ID to use.\n"); $out = implode("\n", $out); return PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($out), 'default', $viewer); } } diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php index 0eadc6dc45..4f0b46aa06 100644 --- a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php +++ b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRuleJIRA.php @@ -1,44 +1,44 @@ getJIRABaseURI(); if ($match_domain != rtrim($jira_base, '/')) { return $matches[0]; } return $this->addDoorkeeperTag( array( 'href' => $matches[0], 'tag' => array( 'ref' => array( DoorkeeperBridgeJIRA::APPTYPE_JIRA, $provider->getProviderDomain(), DoorkeeperBridgeJIRA::OBJTYPE_ISSUE, $match_issue, ), ), )); } } diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php index 804fe18a48..25a6bad994 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php +++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php @@ -1,699 +1,699 @@ getWorkspaceID(); } /** * Publish stories into Asana using the Asana API. */ protected function publishFeedStory() { $story = $this->getFeedStory(); $data = $story->getStoryData(); $viewer = $this->getViewer(); $provider = $this->getProvider(); $workspace_id = $this->getWorkspaceID(); $object = $this->getStoryObject(); $src_phid = $object->getPHID(); $publisher = $this->getPublisher(); // Figure out all the users related to the object. Users go into one of // four buckets: // // - Owner: the owner of the object. This user becomes the assigned owner // of the parent task. // - Active: users who are responsible for the object and need to act on // it. For example, reviewers of a "needs review" revision. // - Passive: users who are responsible for the object, but do not need // to act on it right now. For example, reviewers of a "needs revision" // revision. // - Follow: users who are following the object; generally CCs. $owner_phid = $publisher->getOwnerPHID($object); $active_phids = $publisher->getActiveUserPHIDs($object); $passive_phids = $publisher->getPassiveUserPHIDs($object); $follow_phids = $publisher->getCCUserPHIDs($object); $all_phids = array(); $all_phids = array_merge( array($owner_phid), $active_phids, $passive_phids, $follow_phids); $all_phids = array_unique(array_filter($all_phids)); $phid_aid_map = $this->lookupAsanaUserIDs($all_phids); if (!$phid_aid_map) { throw new PhabricatorWorkerPermanentFailureException( 'No related users have linked Asana accounts.'); } $owner_asana_id = idx($phid_aid_map, $owner_phid); $all_asana_ids = array_select_keys($phid_aid_map, $all_phids); $all_asana_ids = array_values($all_asana_ids); // Even if the actor isn't a reviewer, etc., try to use their account so // we can post in the correct voice. If we miss, we'll try all the other // related users. $try_users = array_merge( array($data->getAuthorPHID()), array_keys($phid_aid_map)); $try_users = array_filter($try_users); $access_info = $this->findAnyValidAsanaAccessToken($try_users); list($possessed_user, $possessed_asana_id, $oauth_token) = $access_info; if (!$oauth_token) { throw new PhabricatorWorkerPermanentFailureException( 'Unable to find any Asana user with valid credentials to '. 'pull an OAuth token out of.'); } $etype_main = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANATASK; $etype_sub = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANASUBTASK; $equery = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($src_phid)) ->withEdgeTypes( array( $etype_main, $etype_sub, )) ->needEdgeData(true); $edges = $equery->execute(); $main_edge = head($edges[$src_phid][$etype_main]); $main_data = $this->getAsanaTaskData($object) + array( 'assignee' => $owner_asana_id, ); $projects = $this->getAsanaProjectIDs(); $extra_data = array(); if ($main_edge) { $extra_data = $main_edge['data']; $refs = id(new DoorkeeperImportEngine()) ->setViewer($possessed_user) ->withPHIDs(array($main_edge['dst'])) ->execute(); $parent_ref = head($refs); if (!$parent_ref) { throw new PhabricatorWorkerPermanentFailureException( 'DoorkeeperExternalObject could not be loaded.'); } if ($parent_ref->getSyncFailed()) { throw new Exception( 'Synchronization of parent task from Asana failed!'); } else if (!$parent_ref->getIsVisible()) { $this->log("Skipping main task update, object is no longer visible.\n"); $extra_data['gone'] = true; } else { $edge_cursor = idx($main_edge['data'], 'cursor', 0); // TODO: This probably breaks, very rarely, on 32-bit systems. if ($edge_cursor <= $story->getChronologicalKey()) { $this->log("Updating main task.\n"); $task_id = $parent_ref->getObjectID(); $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$parent_ref->getObjectID(), 'PUT', $main_data); } else { $this->log( "Skipping main task update, cursor is ahead of the story.\n"); } } } else { // If there are no followers (CCs), and no active or passive users // (reviewers or auditors), and we haven't synchronized the object before, // don't synchronize the object. if (!$active_phids && !$passive_phids && !$follow_phids) { $this->log("Object has no followers or active/passive users.\n"); return; } $parent = $this->makeAsanaAPICall( $oauth_token, 'tasks', 'POST', array( 'workspace' => $workspace_id, 'projects' => $projects, // NOTE: We initially create parent tasks in the "Later" state but // don't update it afterward, even if the corresponding object // becomes actionable. The expectation is that users will prioritize // tasks in responses to notifications of state changes, and that // we should not overwrite their choices. 'assignee_status' => 'later', ) + $main_data); $parent_ref = $this->newRefFromResult( DoorkeeperBridgeAsana::OBJTYPE_TASK, $parent); $extra_data = array( 'workspace' => $workspace_id, ); } // Synchronize main task followers. $task_id = $parent_ref->getObjectID(); // Reviewers are added as followers of the parent task silently, because // they receive a notification when they are assigned as the owner of their // subtask, so the follow notification is redundant / non-actionable. $silent_followers = array_select_keys($phid_aid_map, $active_phids) + array_select_keys($phid_aid_map, $passive_phids); $silent_followers = array_values($silent_followers); // CCs are added as followers of the parent task with normal notifications, // since they won't get a secondary subtask notification. $noisy_followers = array_select_keys($phid_aid_map, $follow_phids); $noisy_followers = array_values($noisy_followers); // To synchronize follower data, just add all the followers. The task might // have additional followers, but we can't really tell how they got there: // were they CC'd and then unsubscribed, or did they manually follow the // task? Assume the latter since it's easier and less destructive and the // former is rare. To be fully consistent, we should enumerate followers // and remove unknown followers, but that's a fair amount of work for little // benefit, and creates a wider window for race conditions. // Add the silent followers first so that a user who is both a reviewer and // a CC gets silently added and then implicitly skipped by then noisy add. // They will get a subtask notification. // We only do this if the task still exists. if (empty($extra_data['gone'])) { $this->addFollowers($oauth_token, $task_id, $silent_followers, true); $this->addFollowers($oauth_token, $task_id, $noisy_followers); // We're also going to synchronize project data here. $this->addProjects($oauth_token, $task_id, $projects); } $dst_phid = $parent_ref->getExternalObject()->getPHID(); // Update the main edge. $edge_data = array( 'cursor' => $story->getChronologicalKey(), ) + $extra_data; $edge_options = array( 'data' => $edge_data, ); id(new PhabricatorEdgeEditor()) ->addEdge($src_phid, $etype_main, $dst_phid, $edge_options) ->save(); if (!$parent_ref->getIsVisible()) { throw new PhabricatorWorkerPermanentFailureException( 'DoorkeeperExternalObject has no visible object on the other side; '. 'this likely indicates the Asana task has been deleted.'); } // Now, handle the subtasks. $sub_editor = new PhabricatorEdgeEditor(); // First, find all the object references in Phabricator for tasks that we // know about and import their objects from Asana. $sub_edges = $edges[$src_phid][$etype_sub]; $sub_refs = array(); $subtask_data = $this->getAsanaSubtaskData($object); $have_phids = array(); if ($sub_edges) { $refs = id(new DoorkeeperImportEngine()) ->setViewer($possessed_user) ->withPHIDs(array_keys($sub_edges)) ->execute(); foreach ($refs as $ref) { if ($ref->getSyncFailed()) { throw new Exception( 'Synchronization of child task from Asana failed!'); } if (!$ref->getIsVisible()) { $ref->getExternalObject()->delete(); continue; } $have_phids[$ref->getExternalObject()->getPHID()] = $ref; } } // Remove any edges in Phabricator which don't have valid tasks in Asana. // These are likely tasks which have been deleted. We're going to respawn // them. foreach ($sub_edges as $sub_phid => $sub_edge) { if (isset($have_phids[$sub_phid])) { continue; } $this->log( "Removing subtask edge to %s, foreign object is not visible.\n", $sub_phid); $sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid); unset($sub_edges[$sub_phid]); } // For each active or passive user, we're looking for an existing, valid // task. If we find one we're going to update it; if we don't, we'll // create one. We ignore extra subtasks that we didn't create (we gain // nothing by deleting them and might be nuking something important) and // ignore subtasks which have been moved across workspaces or replanted // under new parents (this stuff is too edge-casey to bother checking for // and complicated to fix, as it needs extra API calls). However, we do // clean up subtasks we created whose owners are no longer associated // with the object. $subtask_states = array_fill_keys($active_phids, false) + array_fill_keys($passive_phids, true); // Continue with only those users who have Asana credentials. $subtask_states = array_select_keys( $subtask_states, array_keys($phid_aid_map)); $need_subtasks = $subtask_states; $user_to_ref_map = array(); $nuke_refs = array(); foreach ($sub_edges as $sub_phid => $sub_edge) { $user_phid = idx($sub_edge['data'], 'userPHID'); if (isset($need_subtasks[$user_phid])) { unset($need_subtasks[$user_phid]); $user_to_ref_map[$user_phid] = $have_phids[$sub_phid]; } else { // This user isn't associated with the object anymore, so get rid // of their task and edge. $nuke_refs[$sub_phid] = $have_phids[$sub_phid]; } } // These are tasks we know about but which are no longer relevant -- for // example, because a user has been removed as a reviewer. Remove them and // their edges. foreach ($nuke_refs as $sub_phid => $ref) { $sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid); $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$ref->getObjectID(), 'DELETE', array()); $ref->getExternalObject()->delete(); } // For each user that we don't have a subtask for, create a new subtask. foreach ($need_subtasks as $user_phid => $is_completed) { $subtask = $this->makeAsanaAPICall( $oauth_token, 'tasks', 'POST', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], 'completed' => $is_completed, 'parent' => $parent_ref->getObjectID(), )); $subtask_ref = $this->newRefFromResult( DoorkeeperBridgeAsana::OBJTYPE_TASK, $subtask); $user_to_ref_map[$user_phid] = $subtask_ref; // We don't need to synchronize this subtask's state because we just // set it when we created it. unset($subtask_states[$user_phid]); // Add an edge to track this subtask. $sub_editor->addEdge( $src_phid, $etype_sub, $subtask_ref->getExternalObject()->getPHID(), array( 'data' => array( 'userPHID' => $user_phid, ), )); } // Synchronize all the previously-existing subtasks. foreach ($subtask_states as $user_phid => $is_completed) { $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$user_to_ref_map[$user_phid]->getObjectID(), 'PUT', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], 'completed' => $is_completed, )); } foreach ($user_to_ref_map as $user_phid => $ref) { // For each subtask, if the acting user isn't the same user as the subtask // owner, remove the acting user as a follower. Currently, the acting user // will be added as a follower only when they create the task, but this // may change in the future (e.g., closing the task may also mark them // as a follower). Wipe every subtask to be sure. The intent here is to // leave only the owner as a follower so that the acting user doesn't // receive notifications about changes to subtask state. Note that // removing followers is silent in all cases in Asana and never produces // any kind of notification, so this isn't self-defeating. if ($user_phid != $possessed_user->getPHID()) { $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$ref->getObjectID().'/removeFollowers', 'POST', array( 'followers' => array($possessed_asana_id), )); } } // Update edges on our side. $sub_editor->save(); // Don't publish the "create" story, since pushing the object into Asana // naturally generates a notification which effectively serves the same // purpose as the "create" story. Similarly, "close" stories generate a // close notification. if (!$publisher->isStoryAboutObjectCreation($object) && !$publisher->isStoryAboutObjectClosure($object)) { // Post the feed story itself to the main Asana task. We do this last // because everything else is idempotent, so this is the only effect we // can't safely run more than once. $text = $publisher ->setRenderWithImpliedContext(true) ->getStoryText($object); $this->makeAsanaAPICall( $oauth_token, 'tasks/'.$parent_ref->getObjectID().'/stories', 'POST', array( 'text' => $text, )); } } /* -( Internals )---------------------------------------------------------- */ private function getWorkspaceID() { return PhabricatorEnv::getEnvConfig('asana.workspace-id'); } private function getProvider() { if (!$this->provider) { - $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); + $provider = PhabricatorAsanaAuthProvider::getAsanaProvider(); if (!$provider) { throw new PhabricatorWorkerPermanentFailureException( 'No Asana provider configured.'); } $this->provider = $provider; } return $this->provider; } private function getAsanaTaskData($object) { $publisher = $this->getPublisher(); $title = $publisher->getObjectTitle($object); $uri = $publisher->getObjectURI($object); $description = $publisher->getObjectDescription($object); $is_completed = $publisher->isObjectClosed($object); $notes = array( $description, $uri, $this->getSynchronizationWarning(), ); $notes = implode("\n\n", $notes); return array( 'name' => $title, 'notes' => $notes, 'completed' => $is_completed, ); } private function getAsanaSubtaskData($object) { $publisher = $this->getPublisher(); $title = $publisher->getResponsibilityTitle($object); $uri = $publisher->getObjectURI($object); $description = $publisher->getObjectDescription($object); $notes = array( $description, $uri, $this->getSynchronizationWarning(), ); $notes = implode("\n\n", $notes); return array( 'name' => $title, 'notes' => $notes, ); } private function getSynchronizationWarning() { return "\xE2\x9A\xA0 DO NOT EDIT THIS TASK \xE2\x9A\xA0\n". "\xE2\x98\xA0 Your changes will not be reflected in Phabricator.\n". "\xE2\x98\xA0 Your changes will be destroyed the next time state ". "is synchronized."; } private function lookupAsanaUserIDs($all_phids) { $phid_map = array(); $all_phids = array_unique(array_filter($all_phids)); if (!$all_phids) { return $phid_map; } - $provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider(); + $provider = PhabricatorAsanaAuthProvider::getAsanaProvider(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUserPHIDs($all_phids) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); foreach ($accounts as $account) { $phid_map[$account->getUserPHID()] = $account->getAccountID(); } // Put this back in input order. $phid_map = array_select_keys($phid_map, $all_phids); return $phid_map; } private function findAnyValidAsanaAccessToken(array $user_phids) { if (!$user_phids) { return array(null, null, null); } $provider = $this->getProvider(); $viewer = $this->getViewer(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs($user_phids) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); // Reorder accounts in the original order. // TODO: This needs to be adjusted if/when we allow you to link multiple // accounts. $accounts = mpull($accounts, null, 'getUserPHID'); $accounts = array_select_keys($accounts, $user_phids); $workspace_id = $this->getWorkspaceID(); foreach ($accounts as $account) { // Get a token if possible. $token = $provider->getOAuthAccessToken($account); if (!$token) { continue; } // Verify we can actually make a call with the token, and that the user // has access to the workspace in question. try { id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setRawAsanaQuery("workspaces/{$workspace_id}") ->resolve(); } catch (Exception $ex) { // This token didn't make it through; try the next account. continue; } $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($account->getUserPHID())) ->executeOne(); if ($user) { return array($user, $account->getAccountID(), $token); } } return array(null, null, null); } private function makeAsanaAPICall($token, $action, $method, array $params) { foreach ($params as $key => $value) { if ($value === null) { unset($params[$key]); } else if (is_array($value)) { unset($params[$key]); foreach ($value as $skey => $svalue) { $params[$key.'['.$skey.']'] = $svalue; } } } return id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setMethod($method) ->setRawAsanaQuery($action, $params) ->resolve(); } private function newRefFromResult($type, $result) { $ref = id(new DoorkeeperObjectRef()) ->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA) ->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA) ->setObjectType($type) ->setObjectID($result['id']) ->setIsVisible(true); $xobj = $ref->newExternalObject(); $ref->attachExternalObject($xobj); $bridge = new DoorkeeperBridgeAsana(); $bridge->fillObjectFromData($xobj, $result); $xobj->save(); return $ref; } private function addFollowers( $oauth_token, $task_id, array $followers, $silent = false) { if (!$followers) { return; } $data = array( 'followers' => $followers, ); // NOTE: This uses a currently-undocumented API feature to suppress the // follow notifications. if ($silent) { $data['silent'] = true; } $this->makeAsanaAPICall( $oauth_token, "tasks/{$task_id}/addFollowers", 'POST', $data); } private function getAsanaProjectIDs() { $project_ids = array(); $publisher = $this->getPublisher(); $config = PhabricatorEnv::getEnvConfig('asana.project-ids'); if (is_array($config)) { $ids = idx($config, get_class($publisher)); if (is_array($ids)) { foreach ($ids as $id) { if (is_scalar($id)) { $project_ids[] = $id; } } } } return $project_ids; } private function addProjects( $oauth_token, $task_id, array $project_ids) { foreach ($project_ids as $project_id) { $data = array('project' => $project_id); $this->makeAsanaAPICall( $oauth_token, "tasks/{$task_id}/addProject", 'POST', $data); } } } diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php index c7ba08dd70..ed75b55126 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php +++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerJIRA.php @@ -1,174 +1,174 @@ getFeedStory(); $viewer = $this->getViewer(); $provider = $this->getProvider(); $object = $this->getStoryObject(); $publisher = $this->getPublisher(); $jira_issue_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorEdgeConfig::TYPE_PHOB_HAS_JIRAISSUE); if (!$jira_issue_phids) { $this->log("Story is about an object with no linked JIRA issues.\n"); return; } $xobjs = id(new DoorkeeperExternalObjectQuery()) ->setViewer($viewer) ->withPHIDs($jira_issue_phids) ->execute(); if (!$xobjs) { $this->log("Story object has no corresponding external JIRA objects.\n"); return; } $try_users = $this->findUsersToPossess(); if (!$try_users) { $this->log("No users to act on linked JIRA objects.\n"); return; } $story_text = $this->renderStoryText(); $xobjs = mgroup($xobjs, 'getApplicationDomain'); foreach ($xobjs as $domain => $xobj_list) { $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs($try_users) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($domain)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); // Reorder accounts in the original order. // TODO: This needs to be adjusted if/when we allow you to link multiple // accounts. $accounts = mpull($accounts, null, 'getUserPHID'); $accounts = array_select_keys($accounts, $try_users); foreach ($xobj_list as $xobj) { foreach ($accounts as $account) { try { $provider->newJIRAFuture( $account, 'rest/api/2/issue/'.$xobj->getObjectID().'/comment', 'POST', array( 'body' => $story_text, ))->resolveJSON(); break; } catch (HTTPFutureResponseStatus $ex) { phlog($ex); $this->log( "Failed to update object %s using user %s.\n", $xobj->getObjectID(), $account->getUserPHID()); } } } } } /* -( Internals )---------------------------------------------------------- */ /** * Get the active JIRA provider. * - * @return PhabricatorAuthProviderOAuth1JIRA Active JIRA auth provider. + * @return PhabricatorJIRAAuthProvider Active JIRA auth provider. * @task internal */ private function getProvider() { if (!$this->provider) { - $provider = PhabricatorAuthProviderOAuth1JIRA::getJIRAProvider(); + $provider = PhabricatorJIRAAuthProvider::getJIRAProvider(); if (!$provider) { throw new PhabricatorWorkerPermanentFailureException( 'No JIRA provider configured.'); } $this->provider = $provider; } return $this->provider; } /** * Get a list of users to act as when publishing into JIRA. * * @return list Candidate user PHIDs to act as when publishing this * story. * @task internal */ private function findUsersToPossess() { $object = $this->getStoryObject(); $publisher = $this->getPublisher(); $data = $this->getFeedStory()->getStoryData(); // Figure out all the users related to the object. Users go into one of // four buckets. For JIRA integration, we don't care about which bucket // a user is in, since we just want to publish an update to linked objects. $owner_phid = $publisher->getOwnerPHID($object); $active_phids = $publisher->getActiveUserPHIDs($object); $passive_phids = $publisher->getPassiveUserPHIDs($object); $follow_phids = $publisher->getCCUserPHIDs($object); $all_phids = array_merge( array($owner_phid), $active_phids, $passive_phids, $follow_phids); $all_phids = array_unique(array_filter($all_phids)); // Even if the actor isn't a reviewer, etc., try to use their account so // we can post in the correct voice. If we miss, we'll try all the other // related users. $try_users = array_merge( array($data->getAuthorPHID()), $all_phids); $try_users = array_filter($try_users); return $try_users; } private function renderStoryText() { $object = $this->getStoryObject(); $publisher = $this->getPublisher(); $text = $publisher->getStoryText($object); $uri = $publisher->getObjectURI($object); return $text."\n\n".$uri; } } diff --git a/src/applications/people/controller/PhabricatorPeopleController.php b/src/applications/people/controller/PhabricatorPeopleController.php index ce24c141e9..0061977bfb 100644 --- a/src/applications/people/controller/PhabricatorPeopleController.php +++ b/src/applications/people/controller/PhabricatorPeopleController.php @@ -1,51 +1,51 @@ setBaseURI(new PhutilURI($this->getApplicationURI())); $viewer = $this->getRequest()->getUser(); id(new PhabricatorPeopleSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); if ($viewer->getIsAdmin()) { $nav->addLabel(pht('User Administration')); - if (PhabricatorAuthProviderLDAP::getLDAPProvider()) { + if (PhabricatorLDAPAuthProvider::getLDAPProvider()) { $nav->addFilter('ldap', pht('Import from LDAP')); } $nav->addFilter('logs', pht('Activity Logs')); } return $nav; } public function buildApplicationMenu() { return $this->buildSideNavView()->getMenu(); } public function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $viewer = $this->getRequest()->getUser(); if ($viewer->getIsAdmin()) { $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create New User')) ->setHref($this->getApplicationURI('create/')) ->setIcon('fa-plus-square')); } return $crumbs; } } diff --git a/src/applications/people/controller/PhabricatorPeopleLdapController.php b/src/applications/people/controller/PhabricatorPeopleLdapController.php index 2e12941788..d373399e04 100644 --- a/src/applications/people/controller/PhabricatorPeopleLdapController.php +++ b/src/applications/people/controller/PhabricatorPeopleLdapController.php @@ -1,217 +1,217 @@ getRequest(); $admin = $request->getUser(); $content = array(); $form = id(new AphrontFormView()) ->setAction($request->getRequestURI() ->alter('search', 'true')->alter('import', null)) ->setUser($admin) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('LDAP username')) ->setName('username')) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('LDAP query')) ->setCaption(pht('A filter such as (objectClass=*)')) ->setName('query')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Search'))); $panel = id(new AphrontPanelView()) ->setHeader(pht('Import LDAP Users')) ->setNoBackground() ->setWidth(AphrontPanelView::WIDTH_FORM) ->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Import Ldap Users'), $this->getApplicationURI('/ldap/')); $nav = $this->buildSideNavView(); $nav->setCrumbs($crumbs); $nav->selectFilter('ldap'); $nav->appendChild($content); if ($request->getStr('import')) { $nav->appendChild($this->processImportRequest($request)); } $nav->appendChild($panel); if ($request->getStr('search')) { $nav->appendChild($this->processSearchRequest($request)); } return $this->buildApplicationPage( $nav, array( 'title' => pht('Import Ldap Users'), )); } private function processImportRequest($request) { $admin = $request->getUser(); $usernames = $request->getArr('usernames'); $emails = $request->getArr('email'); $names = $request->getArr('name'); $notice_view = new AphrontErrorView(); $notice_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice_view->setTitle(pht('Import Successful')); $notice_view->setErrors(array( pht('Successfully imported users from LDAP'), )); $list = new PHUIObjectItemListView(); $list->setNoDataString(pht('No users imported?')); foreach ($usernames as $username) { $user = new PhabricatorUser(); $user->setUsername($username); $user->setRealname($names[$username]); $email_obj = id(new PhabricatorUserEmail()) ->setAddress($emails[$username]) ->setIsVerified(1); try { id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email_obj); id(new PhabricatorExternalAccount()) ->setUserPHID($user->getPHID()) ->setAccountType('ldap') ->setAccountDomain('self') ->setAccountID($username) ->save(); $header = pht('Successfully added %s', $username); $attribute = null; $color = 'green'; } catch (Exception $ex) { $header = pht('Failed to add %s', $username); $attribute = $ex->getMessage(); $color = 'red'; } $item = id(new PHUIObjectItemView()) ->setHeader($header) ->addAttribute($attribute) ->setBarColor($color); $list->addItem($item); } return array( $notice_view, $list, ); } private function processSearchRequest($request) { $panel = new AphrontPanelView(); $admin = $request->getUser(); $search = $request->getStr('query'); - $ldap_provider = PhabricatorAuthProviderLDAP::getLDAPProvider(); + $ldap_provider = PhabricatorLDAPAuthProvider::getLDAPProvider(); if (!$ldap_provider) { throw new Exception('No LDAP provider enabled!'); } $ldap_adapter = $ldap_provider->getAdapter(); $ldap_adapter->setLoginUsername($request->getStr('username')); $ldap_adapter->setLoginPassword( new PhutilOpaqueEnvelope($request->getStr('password'))); // This causes us to connect and bind. // TODO: Clean up this discard mode stuff. DarkConsoleErrorLogPluginAPI::enableDiscardMode(); $ldap_adapter->getAccountID(); DarkConsoleErrorLogPluginAPI::disableDiscardMode(); $results = $ldap_adapter->searchLDAP('%Q', $search); foreach ($results as $key => $record) { $account_id = $ldap_adapter->readLDAPRecordAccountID($record); if (!$account_id) { unset($results[$key]); continue; } $info = array( $account_id, $ldap_adapter->readLDAPRecordEmail($record), $ldap_adapter->readLDAPRecordRealName($record), ); $results[$key] = $info; $results[$key][] = $this->renderUserInputs($info); } $form = id(new AphrontFormView()) ->setUser($admin); $table = new AphrontTableView($results); $table->setHeaders( array( pht('Username'), pht('Email'), pht('Real Name'), pht('Import?'), )); $form->appendChild($table); $form->setAction($request->getRequestURI() ->alter('import', 'true')->alter('search', null)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Import'))); $panel->appendChild($form); return $panel; } private function renderUserInputs($user) { $username = $user[0]; return hsprintf( '%s%s%s', phutil_tag( 'input', array( 'type' => 'checkbox', 'name' => 'usernames[]', 'value' => $username, )), phutil_tag( 'input', array( 'type' => 'hidden', 'name' => "email[$username]", 'value' => $user[1], )), phutil_tag( 'input', array( 'type' => 'hidden', 'name' => "name[$username]", 'value' => $user[2], ))); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index ed9ac15fa0..bef873c09e 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,886 +1,886 @@ timezoneIdentifier, date_default_timezone_get()); // Make sure these return booleans. case 'isAdmin': return (bool)$this->isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; case 'isEmailVerified': return (bool)$this->isEmailVerified; case 'isApproved': return (bool)$this->isApproved; default: return parent::readField($field); } } /** * Is this a live account which has passed required approvals? Returns true * if this is an enabled, verified (if required), approved (if required) * account, and false otherwise. * * @return bool True if this is a standard, usable account. */ public function isUserActivated() { if ($this->getIsDisabled()) { return false; } if (!$this->getIsApproved()) { return false; } if (PhabricatorUserEmail::isEmailVerificationRequired()) { if (!$this->getIsEmailVerified()) { return false; } } return true; } /** * Returns `true` if this is a standard user who is logged in. Returns `false` * for logged out, anonymous, or external users. * * @return bool `true` if the user is a standard user who is logged in with * a normal session. */ public function getIsStandardUser() { $type_user = PhabricatorPeoplePHIDTypeUser::TYPECONST; return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeoplePHIDTypeUser::TYPECONST); } public function setPassword(PhutilOpaqueEnvelope $envelope) { if (!$this->getPHID()) { throw new Exception( 'You can not set a password for an unsaved user because their PHID '. 'is a salt component in the password hash.'); } if (!strlen($envelope->openEnvelope())) { $this->setPasswordHash(''); } else { $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32))); $hash = $this->hashPassword($envelope); $this->setPasswordHash($hash->openEnvelope()); } return $this; } // To satisfy PhutilPerson. public function getSex() { return $this->sex; } public function getMonogram() { return '@'.$this->getUsername(); } public function getTranslation() { try { if ($this->translation && class_exists($this->translation) && is_subclass_of($this->translation, 'PhabricatorTranslation')) { return $this->translation; } } catch (PhutilMissingSymbolException $ex) { return null; } return null; } public function isLoggedIn() { return !($this->getPHID() === null); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } if (!strlen($this->getAccountSecret())) { $this->setAccountSecret(Filesystem::readRandomCharacters(64)); } $result = parent::save(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($this->getPHID()); return $result; } public function attachSession(PhabricatorAuthSession $session) { $this->session = $session; return $this; } public function getSession() { return $this->assertAttached($this->session); } public function hasSession() { return ($this->session !== self::ATTACHABLE); } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } public function comparePassword(PhutilOpaqueEnvelope $envelope) { if (!strlen($envelope->openEnvelope())) { return false; } if (!strlen($this->getPasswordHash())) { return false; } return PhabricatorPasswordHasher::comparePassword( $this->getPasswordHashInput($envelope), new PhutilOpaqueEnvelope($this->getPasswordHash())); } private function getPasswordHashInput(PhutilOpaqueEnvelope $password) { $input = $this->getUsername(). $password->openEnvelope(). $this->getPHID(). $this->getPasswordSalt(); return new PhutilOpaqueEnvelope($input); } private function hashPassword(PhutilOpaqueEnvelope $password) { $hasher = PhabricatorPasswordHasher::getBestHasher(); $input_envelope = $this->getPasswordHashInput($password); return $hasher->getPasswordHashForStorage($input_envelope); } const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_SALT_LENGTH = 8; const CSRF_TOKEN_LENGTH = 16; const CSRF_BREACH_PREFIX = 'B@'; const EMAIL_CYCLE_FREQUENCY = 86400; const EMAIL_TOKEN_LENGTH = 24; private function getRawCSRFToken($offset = 0) { return $this->generateToken( time() + (self::CSRF_CYCLE_FREQUENCY * $offset), self::CSRF_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), self::CSRF_TOKEN_LENGTH); } /** * @phutil-external-symbol class PhabricatorStartup */ public function getCSRFToken() { $salt = PhabricatorStartup::getGlobal('csrf.salt'); if (!$salt) { $salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH); PhabricatorStartup::setGlobal('csrf.salt', $salt); } // Generate a token hash to mitigate BREACH attacks against SSL. See // discussion in T3684. $token = $this->getRawCSRFToken(); $hash = PhabricatorHash::digest($token, $salt); return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { $salt = null; $version = 'plain'; // This is a BREACH-mitigating token. See T3684. $breach_prefix = self::CSRF_BREACH_PREFIX; $breach_prelen = strlen($breach_prefix); if (!strncmp($token, $breach_prefix, $breach_prelen)) { $version = 'breach'; $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH); $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH); } // When the user posts a form, we check that it contains a valid CSRF token. // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept // either the current token, the next token (users can submit a "future" // token if you have two web frontends that have some clock skew) or any of // the last 6 tokens. This means that pages are valid for up to 7 hours. // There is also some Javascript which periodically refreshes the CSRF // tokens on each page, so theoretically pages should be valid indefinitely. // However, this code may fail to run (if the user loses their internet // connection, or there's a JS problem, or they don't have JS enabled). // Choosing the size of the window in which we accept old CSRF tokens is // an issue of balancing concerns between security and usability. We could // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to // attacks using captured CSRF tokens, but it's also more likely that real // users will be affected by this, e.g. if they close their laptop for an // hour, open it back up, and try to submit a form before the CSRF refresh // can kick in. Since the user experience of submitting a form with expired // CSRF is often quite bad (you basically lose data, or it's a big pain to // recover at least) and I believe we gain little additional protection // by keeping the window very short (the overwhelming value here is in // preventing blind attacks, and most attacks which can capture CSRF tokens // can also just capture authentication information [sniffing networks] // or act as the user [xss]) the 7 hour default seems like a reasonable // balance. Other major platforms have much longer CSRF token lifetimes, // like Rails (session duration) and Django (forever), which suggests this // is a reasonable analysis. $csrf_window = 6; for ($ii = -$csrf_window; $ii <= 1; $ii++) { $valid = $this->getRawCSRFToken($ii); switch ($version) { // TODO: We can remove this after the BREACH version has been in the // wild for a while. case 'plain': if ($token == $valid) { return true; } break; case 'breach': $digest = PhabricatorHash::digest($valid, $salt); if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) { return true; } break; default: throw new Exception('Unknown CSRF token format!'); } } return false; } private function generateToken($epoch, $frequency, $key, $len) { if ($this->getPHID()) { $vec = $this->getPHID().$this->getAccountSecret(); } else { $vec = $this->getAlternateCSRFString(); } $time_block = floor($epoch / $frequency); $vec = $vec.$key.$time_block; return substr(PhabricatorHash::digest($vec), 0, $len); } public function attachUserProfile(PhabricatorUserProfile $profile) { $this->profile = $profile; return $this; } public function loadUserProfile() { if ($this->profile) { return $this->profile; } $profile_dao = new PhabricatorUserProfile(); $this->profile = $profile_dao->loadOneWhere('userPHID = %s', $this->getPHID()); if (!$this->profile) { $profile_dao->setUserPHID($this->getPHID()); $this->profile = $profile_dao; } return $this->profile; } public function loadPrimaryEmailAddress() { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception('User has no primary email address!'); } return $email->getAddress(); } public function loadPrimaryEmail() { return $this->loadOneRelative( new PhabricatorUserEmail(), 'userPHID', 'getPHID', '(isPrimary = 1)'); } public function loadPreferences() { if ($this->preferences) { return $this->preferences; } $preferences = null; if ($this->getPHID()) { $preferences = id(new PhabricatorUserPreferences())->loadOneWhere( 'userPHID = %s', $this->getPHID()); } if (!$preferences) { $preferences = new PhabricatorUserPreferences(); $preferences->setUserPHID($this->getPHID()); $default_dict = array( PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', PhabricatorUserPreferences::PREFERENCE_EDITOR => '', PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '', PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0); $preferences->setPreferences($default_dict); } $this->preferences = $preferences; return $preferences; } public function loadEditorLink($path, $line, $callsign) { $editor = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_EDITOR); if (is_array($path)) { $multiedit = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MULTIEDIT); switch ($multiedit) { case '': $path = implode(' ', $path); break; case 'disable': return null; } } if (!strlen($editor)) { return null; } $uri = strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); // The resulting URI must have an allowed protocol. Otherwise, we'll return // a link to an error page explaining the misconfiguration. $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri); if (!$ok) { return '/help/editorprotocol/'; } return (string)$uri; } public function getAlternateCSRFString() { return $this->assertAttached($this->alternateCSRFString); } public function attachAlternateCSRFString($string) { $this->alternateCSRFString = $string; return $this; } /** * Populate the nametoken table, which used to fetch typeahead results. When * a user types "linc", we want to match "Abraham Lincoln" from on-demand * typeahead sources. To do this, we need a separate table of name fragments. */ public function updateNameTokens() { $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $tokens = PhabricatorTypeaheadDatasource::tokenizeString( $this->getUserName().' '.$this->getRealName()); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $this->getID(), $token); } queryfx( $conn_w, 'DELETE FROM %T WHERE userID = %d', $table, $this->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (userID, token) VALUES %Q', $table, implode(', ', $sql)); } } public function sendWelcomeEmail(PhabricatorUser $admin) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $base_uri = PhabricatorEnv::getProductionURI('/'); $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, $this->loadPrimaryEmail(), PhabricatorAuthSessionEngine::ONETIME_WELCOME); $body = <<addTos(array($this->getPHID())) ->setSubject('[Phabricator] Welcome to Phabricator') ->setBody($body) ->saveAndSend(); } public function sendUsernameChangeEmail( PhabricatorUser $admin, $old_username) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $new_username = $this->getUserName(); $password_instructions = null; - if (PhabricatorAuthProviderPassword::getPasswordProvider()) { + if (PhabricatorPasswordAuthProvider::getPasswordProvider()) { $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, null, PhabricatorAuthSessionEngine::ONETIME_USERNAME); $password_instructions = <<addTos(array($this->getPHID())) ->setSubject('[Phabricator] Username Changed') ->setBody($body) ->saveAndSend(); } public static function describeValidUsername() { return pht( 'Usernames must contain only numbers, letters, period, underscore and '. 'hyphen, and can not end with a period. They must have no more than %d '. 'characters.', new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); } public static function validateUsername($username) { // NOTE: If you update this, make sure to update: // // - Remarkup rule for @mentions. // - Routing rule for "/p/username/". // - Unit tests, obviously. // - describeValidUsername() method, above. if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) { return false; } return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username); } public static function getDefaultProfileImageURI() { return celerity_get_resource_uri('/rsrc/image/avatar.png'); } public function attachStatus(PhabricatorCalendarEvent $status) { $this->status = $status; return $this; } public function getStatus() { return $this->assertAttached($this->status); } public function hasStatus() { return $this->status !== self::ATTACHABLE; } public function attachProfileImageURI($uri) { $this->profileImage = $uri; return $this; } public function getProfileImageURI() { return $this->assertAttached($this->profileImage); } public function loadProfileImageURI() { if ($this->profileImage && ($this->profileImage !== self::ATTACHABLE)) { return $this->profileImage; } $src_phid = $this->getProfileImagePHID(); if ($src_phid) { // TODO: (T603) Can we get rid of this entirely and move it to // PeopleQuery with attach/attachable? $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); if ($file) { $this->profileImage = $file->getBestURI(); return $this->profileImage; } } $this->profileImage = self::getDefaultProfileImageURI(); return $this->profileImage; } public function getFullName() { if (strlen($this->getRealName())) { return $this->getUsername().' ('.$this->getRealName().')'; } else { return $this->getUsername(); } } public function __toString() { return $this->getUsername(); } public static function loadOneWithEmailAddress($address) { $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $address); if (!$email) { return null; } return id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $email->getUserPHID()); } /* -( Multi-Factor Authentication )---------------------------------------- */ /** * Update the flag storing this user's enrollment in multi-factor auth. * * With certain settings, we need to check if a user has MFA on every page, * so we cache MFA enrollment on the user object for performance. Calling this * method synchronizes the cache by examining enrollment records. After * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if * the user is enrolled. * * This method should be called after any changes are made to a given user's * multi-factor configuration. * * @return void * @task factors */ public function updateMultiFactorEnrollment() { $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); $enrolled = count($factors) ? 1 : 0; if ($enrolled !== $this->isEnrolledInMultiFactor) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d', $this->getTableName(), $enrolled, $this->getID()); unset($unguarded); $this->isEnrolledInMultiFactor = $enrolled; } } /** * Check if the user is enrolled in multi-factor authentication. * * Enrolled users have one or more multi-factor authentication sources * attached to their account. For performance, this value is cached. You * can use @{method:updateMultiFactorEnrollment} to update the cache. * * @return bool True if the user is enrolled. * @task factors */ public function getIsEnrolledInMultiFactor() { return $this->isEnrolledInMultiFactor; } /* -( Omnipotence )-------------------------------------------------------- */ /** * Returns true if this user is omnipotent. Omnipotent users bypass all policy * checks. * * @return bool True if the user bypasses policy checks. */ public function isOmnipotent() { return $this->omnipotent; } /** * Get an omnipotent user object for use in contexts where there is no acting * user, notably daemons. * * @return PhabricatorUser An omnipotent user. */ public static function getOmnipotentUser() { static $user = null; if (!$user) { $user = new PhabricatorUser(); $user->omnipotent = true; $user->makeEphemeral(); } return $user; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_PUBLIC; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getIsSystemAgent()) { return PhabricatorPolicies::POLICY_ADMIN; } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getPHID() && ($viewer->getPHID() === $this->getPHID()); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht('Only you can edit your information.'); default: return null; } } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('user.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorUserCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($externals as $external) { $external->delete(); } $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($prefs as $pref) { $pref->delete(); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($keys as $key) { $key->delete(); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { $email->delete(); } $sessions = id(new PhabricatorAuthSession())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($sessions as $session) { $session->delete(); } $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($factors as $factor) { $factor->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index c142b1da6c..cb497c61b4 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -1,230 +1,230 @@ setBloggerPHID($blogger->getPHID()) ->setBlogPHID($blog->getPHID()) ->setBlog($blog) ->setDatePublished(0) ->setVisibility(self::VISIBILITY_DRAFT); return $post; } public function setBlog(PhameBlog $blog) { $this->blog = $blog; return $this; } public function getBlog() { return $this->blog; } public function getViewURI() { // go for the pretty uri if we can $domain = ($this->blog ? $this->blog->getDomain() : ''); if ($domain) { $phame_title = PhabricatorSlug::normalize($this->getPhameTitle()); return 'http://'.$domain.'/post/'.$phame_title; } $uri = '/phame/post/view/'.$this->getID().'/'; return PhabricatorEnv::getProductionURI($uri); } public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } public function isDraft() { return $this->getVisibility() == self::VISIBILITY_DRAFT; } public function getHumanName() { if ($this->isDraft()) { $name = 'draft'; } else { $name = 'post'; } return $name; } public function getCommentsWidget() { $config_data = $this->getConfigData(); if (empty($config_data)) { return 'none'; } return idx($config_data, 'comments_widget', 'none'); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhamePHIDTypePost::TYPECONST); } public function toDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'blogPHID' => $this->getBlogPHID(), 'bloggerPHID' => $this->getBloggerPHID(), 'viewURI' => $this->getViewURI(), 'title' => $this->getTitle(), 'phameTitle' => $this->getPhameTitle(), 'body' => $this->getBody(), 'summary' => PhabricatorMarkupEngine::summarize($this->getBody()), 'datePublished' => $this->getDatePublished(), 'published' => !$this->isDraft(), ); } public static function getVisibilityOptionsForSelect() { return array( self::VISIBILITY_DRAFT => 'Draft: visible only to me.', self::VISIBILITY_PUBLISHED => 'Published: visible to the whole world.', ); } public function getCommentsWidgetOptionsForSelect() { $current = $this->getCommentsWidget(); $options = array(); if ($current == 'facebook' || - PhabricatorAuthProviderOAuthFacebook::getFacebookApplicationID()) { + PhabricatorFacebookAuthProvider::getFacebookApplicationID()) { $options['facebook'] = 'Facebook'; } if ($current == 'disqus' || PhabricatorEnv::getEnvConfig('disqus.shortname')) { $options['disqus'] = 'Disqus'; } $options['none'] = 'None'; return $options; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // Draft posts are visible only to the author. Published posts are visible // to whoever the blog is visible to. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->isDraft() && $this->getBlog()) { return $this->getBlog()->getViewPolicy(); } else { return PhabricatorPolicies::POLICY_NOONE; } break; case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A blog post's author can always view it, and is the only user allowed // to edit it. switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: return ($user->getPHID() == $this->getBloggerPHID()); } } public function describeAutomaticCapability($capability) { return pht( 'The author of a blog post can always view and edit it.'); } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { switch ($field) { case self::MARKUP_FIELD_BODY: return $this->getBody(); case self::MARKUP_FIELD_SUMMARY: return PhabricatorMarkupEngine::summarize($this->getBody()); } } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getBloggerPHID(), ); } } diff --git a/src/applications/phame/view/PhamePostView.php b/src/applications/phame/view/PhamePostView.php index b816f6206a..5f60b26f18 100644 --- a/src/applications/phame/view/PhamePostView.php +++ b/src/applications/phame/view/PhamePostView.php @@ -1,240 +1,240 @@ skin = $skin; return $this; } public function getSkin() { return $this->skin; } public function setAuthor(PhabricatorObjectHandle $author) { $this->author = $author; return $this; } public function getAuthor() { return $this->author; } public function setPost(PhamePost $post) { $this->post = $post; return $this; } public function getPost() { return $this->post; } public function setBody($body) { $this->body = $body; return $this; } public function getBody() { return $this->body; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { return $this->summary; } public function renderTitle() { $href = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); return phutil_tag( 'h2', array( 'class' => 'phame-post-title', ), phutil_tag( 'a', array( 'href' => $href, ), $this->getPost()->getTitle())); } public function renderDatePublished() { return phutil_tag( 'div', array( 'class' => 'phame-post-date', ), pht( 'Published on %s by %s', phabricator_datetime( $this->getPost()->getDatePublished(), $this->getUser()), $this->getAuthor()->getName())); } public function renderBody() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getBody()); } public function renderSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getSummary()); } public function renderComments() { $post = $this->getPost(); switch ($post->getCommentsWidget()) { case 'facebook': $comments = $this->renderFacebookComments(); break; case 'disqus': $comments = $this->renderDisqusComments(); break; case 'none': default: $comments = null; break; } return $comments; } public function render() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderBody(), $this->renderComments(), )); } public function renderWithSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderSummary(), )); } private function renderFacebookComments() { - $fb_id = PhabricatorAuthProviderOAuthFacebook::getFacebookApplicationID(); + $fb_id = PhabricatorFacebookAuthProvider::getFacebookApplicationID(); if (!$fb_id) { return null; } $fb_root = phutil_tag('div', array( 'id' => 'fb-root', ), ''); $c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id; $fb_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( '(function(d, s, id) {'. ' var js, fjs = d.getElementsByTagName(s)[0];'. ' if (d.getElementById(id)) return;'. ' js = d.createElement(s); js.id = id;'. ' js.src = %s;'. ' fjs.parentNode.insertBefore(js, fjs);'. '}(document, \'script\', \'facebook-jssdk\'));', $c_uri)); $uri = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); $fb_comments = phutil_tag('div', array( 'class' => 'fb-comments', 'data-href' => $uri, 'data-num-posts' => 5, ), ''); return phutil_tag( 'div', array( 'class' => 'phame-comments-facebook', ), array( $fb_root, $fb_js, $fb_comments, )); } private function renderDisqusComments() { $disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname'); if (!$disqus_shortname) { return null; } $post = $this->getPost(); $disqus_thread = phutil_tag('div', array( 'id' => 'disqus_thread' )); // protip - try some var disqus_developer = 1; action to test locally $disqus_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( ' var disqus_shortname = %s;'. ' var disqus_identifier = %s;'. ' var disqus_url = %s;'. ' var disqus_title = %s;'. '(function() {'. ' var dsq = document.createElement("script");'. ' dsq.type = "text/javascript";'. ' dsq.async = true;'. ' dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";'. '(document.getElementsByTagName("head")[0] ||'. ' document.getElementsByTagName("body")[0]).appendChild(dsq);'. '})();', $disqus_shortname, $post->getPHID(), $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()), $post->getTitle())); return phutil_tag( 'div', array( 'class' => 'phame-comments-disqus', ), array( $disqus_thread, $disqus_js, )); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php index 73c9c490b4..14872a227e 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php @@ -1,190 +1,190 @@ getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $user, $request, '/settings/'); $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); $min_len = (int)$min_len; // NOTE: To change your password, you need to prove you own the account, // either by providing the old password or by carrying a token to // the workflow from a password reset email. $key = $request->getStr('key'); $token = null; if ($key) { $token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($user) ->withObjectPHIDs(array($user->getPHID())) ->withTokenTypes( array(PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE)) ->withTokenCodes(array(PhabricatorHash::digest($key))) ->withExpired(false) ->executeOne(); } $e_old = true; $e_new = true; $e_conf = true; $errors = array(); if ($request->isFormPost()) { if (!$token) { $envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw')); if (!$user->comparePassword($envelope)) { $errors[] = pht('The old password you entered is incorrect.'); $e_old = pht('Invalid'); } } $pass = $request->getStr('new_pw'); $conf = $request->getStr('conf_pw'); if (strlen($pass) < $min_len) { $errors[] = pht('Your new password is too short.'); $e_new = pht('Too Short'); } else if ($pass !== $conf) { $errors[] = pht('New password and confirmation do not match.'); $e_conf = pht('Invalid'); } else if (PhabricatorCommonPasswords::isCommonPassword($pass)) { $e_new = pht('Very Weak'); $e_conf = pht('Very Weak'); $errors[] = pht( 'Your new password is very weak: it is one of the most common '. 'passwords in use. Choose a stronger password.'); } if (!$errors) { // This write is unguarded because the CSRF token has already // been checked in the call to $request->isFormPost() and // the CSRF token depends on the password hash, so when it // is changed here the CSRF token check will fail. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $envelope = new PhutilOpaqueEnvelope($pass); id(new PhabricatorUserEditor()) ->setActor($user) ->changePassword($user, $envelope); unset($unguarded); if ($token) { // Destroy the token. $token->delete(); // If this is a password set/reset, kick the user to the home page // after we update their account. $next = '/'; } else { $next = $this->getPanelURI('?saved=true'); } return id(new AphrontRedirectResponse())->setURI($next); } } $hash_envelope = new PhutilOpaqueEnvelope($user->getPasswordHash()); if (strlen($hash_envelope->openEnvelope())) { if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { $errors[] = pht( 'The strength of your stored password hash can be upgraded. '. 'To upgrade, either: log out and log in using your password; or '. 'change your password.'); } } $len_caption = null; if ($min_len) { $len_caption = pht('Minimum password length: %d characters.', $min_len); } $form = new AphrontFormView(); $form ->setUser($user) ->addHiddenInput('key', $key); if (!$token) { $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Old Password')) ->setError($e_old) ->setName('old_pw')); } $form ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('New Password')) ->setError($e_new) ->setName('new_pw')); $form ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Confirm Password')) ->setCaption($len_caption) ->setError($e_conf) ->setName('conf_pw')); $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Change Password'))); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Current Algorithm')) ->setValue(PhabricatorPasswordHasher::getCurrentAlgorithmName( new PhutilOpaqueEnvelope($user->getPasswordHash())))); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Best Available Algorithm')) ->setValue(PhabricatorPasswordHasher::getBestAlgorithmName())); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Change Password')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setForm($form); return array( $form_box, ); } }