diff --git a/src/applications/auth/controller/PhabricatorLogoutController.php b/src/applications/auth/controller/PhabricatorLogoutController.php index f75b4c601d..3949d45b89 100644 --- a/src/applications/auth/controller/PhabricatorLogoutController.php +++ b/src/applications/auth/controller/PhabricatorLogoutController.php @@ -1,60 +1,60 @@ getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { - $log = PhabricatorUserLog::newLog( - $user, + $log = PhabricatorUserLog::initializeNewLog( $user, + $user->getPHID(), PhabricatorUserLog::ACTION_LOGOUT); $log->save(); // Destroy the user's session in the database so logout works even if // their cookies have some issues. We'll detect cookie issues when they // try to login again and tell them to clear any junk. $phsid = $request->getCookie('phsid'); if ($phsid) { $user->destroySession($phsid); } $request->clearCookie('phsid'); return id(new AphrontRedirectResponse()) ->setURI('/login/'); } if ($user->getPHID()) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Log out of Phabricator?')) ->appendChild(phutil_tag('p', array(), pht( 'Are you sure you want to log out?'))) ->addSubmitButton(pht('Logout')) ->addCancelButton('/'); return id(new AphrontDialogResponse())->setDialog($dialog); } return id(new AphrontRedirectResponse())->setURI('/'); } } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderPassword.php b/src/applications/auth/provider/PhabricatorAuthProviderPassword.php index ab4875b82b..28c5ae79d7 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderPassword.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderPassword.php @@ -1,253 +1,253 @@ adapter) { $adapter = new PhutilAuthAdapterEmpty(); $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('phusr')); $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 (!$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 (!$account) { - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( null, - $log_user, + $log_user ? $log_user->getPHID() : null, PhabricatorUserLog::ACTION_LOGIN_FAILURE); $log->save(); $request->clearCookie('phusr'); $request->clearCookie('phsid'); $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) { return $provider; } } return null; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { return; } public function shouldAllowAccountRefresh() { return false; } } diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php index 7f72f0f834..39aa5564da 100644 --- a/src/applications/people/controller/PhabricatorPeopleEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleEditController.php @@ -1,801 +1,801 @@ id = idx($data, 'id'); $this->view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $admin = $request->getUser(); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); if ($this->id) { $user = id(new PhabricatorUser())->load($this->id); if (!$user) { return new Aphront404Response(); } $base_uri = '/people/edit/'.$user->getID().'/'; $crumbs->addTextCrumb(pht('Edit User'), '/people/edit/'); $crumbs->addTextCrumb($user->getFullName(), $base_uri); } else { $user = new PhabricatorUser(); $base_uri = '/people/edit/'; $crumbs->addTextCrumb(pht('Create New User'), $base_uri); } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($base_uri)); $nav->addLabel(pht('User Information')); $nav->addFilter('basic', pht('Basic Information')); $nav->addFilter('role', pht('Edit Roles')); $nav->addFilter('cert', pht('Conduit Certificate')); $nav->addFilter('profile', pht('View Profile'), '/p/'.$user->getUsername().'/'); $nav->addLabel(pht('Special')); $nav->addFilter('rename', pht('Change Username')); if ($user->getIsSystemAgent()) { $nav->addFilter('picture', pht('Set Account Picture')); } $nav->addFilter('delete', pht('Delete User')); if (!$user->getID()) { $this->view = 'basic'; } $view = $nav->selectFilter($this->view, 'basic'); $content = array(); if ($request->getStr('saved')) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Changes Saved')); $notice->appendChild( phutil_tag('p', array(), pht('Your changes were saved.'))); $content[] = $notice; } switch ($view) { case 'basic': $response = $this->processBasicRequest($user); break; case 'role': $response = $this->processRoleRequest($user); break; case 'cert': $response = $this->processCertificateRequest($user); break; case 'rename': $response = $this->processRenameRequest($user); break; case 'picture': $response = $this->processSetAccountPicture($user); break; case 'delete': $response = $this->processDeleteRequest($user); break; default: return new Aphront404Response(); } if ($response instanceof AphrontResponse) { return $response; } $content[] = $response; if ($user->getID()) { $nav->appendChild($content); } else { $nav = $this->buildSideNavView(); $nav->selectFilter('edit'); $nav->appendChild($content); } $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => pht('Edit User'), 'device' => true, )); } private function processBasicRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $e_username = true; $e_realname = true; $e_email = true; $errors = array(); $welcome_checked = true; $new_email = null; $request = $this->getRequest(); if ($request->isFormPost()) { $welcome_checked = $request->getInt('welcome'); $is_new = !$user->getID(); if ($is_new) { $user->setUsername($request->getStr('username')); $new_email = $request->getStr('email'); if (!strlen($new_email)) { $errors[] = pht('Email is required.'); $e_email = pht('Required'); } else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } } $user->setRealName($request->getStr('realname')); if (!strlen($user->getUsername())) { $errors[] = pht("Username is required."); $e_username = pht('Required'); } else if (!PhabricatorUser::validateUsername($user->getUsername())) { $errors[] = PhabricatorUser::describeValidUsername(); $e_username = pht('Invalid'); } else { $e_username = null; } if (!strlen($user->getRealName())) { $errors[] = pht('Real name is required.'); $e_realname = pht('Required'); } else { $e_realname = null; } if (!$errors) { try { if (!$is_new) { id(new PhabricatorUserEditor()) ->setActor($admin) ->updateUser($user); } else { $email = id(new PhabricatorUserEmail()) ->setAddress($new_email) ->setIsVerified(0); // Automatically approve the user, since an admin is creating them. $user->setIsApproved(1); id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email); if ($request->getStr('role') == 'agent') { id(new PhabricatorUserEditor()) ->setActor($admin) ->makeSystemAgentUser($user, true); } } if ($welcome_checked) { $user->sendWelcomeEmail($admin); } $response = id(new AphrontRedirectResponse()) ->setURI('/people/edit/'.$user->getID().'/?saved=true'); return $response; } catch (AphrontQueryDuplicateKeyException $ex) { $errors[] = pht('Username and email must be unique.'); $same_username = id(new PhabricatorUser()) ->loadOneWhere('username = %s', $user->getUsername()); $same_email = id(new PhabricatorUserEmail()) ->loadOneWhere('address = %s', $new_email); if ($same_username) { $e_username = pht('Duplicate'); } if ($same_email) { $e_email = pht('Duplicate'); } } } } $form = new AphrontFormView(); $form->setUser($admin); if ($user->getID()) { $form->setAction('/people/edit/'.$user->getID().'/'); } else { $form->setAction('/people/edit/'); } if ($user->getID()) { $is_immutable = true; } else { $is_immutable = false; } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username')) ->setName('username') ->setValue($user->getUsername()) ->setError($e_username) ->setDisabled($is_immutable)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Real Name')) ->setName('realname') ->setValue($user->getRealName()) ->setError($e_realname)); if (!$user->getID()) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setDisabled($is_immutable) ->setValue($new_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); } else { $email = $user->loadPrimaryEmail(); if ($email) { $status = $email->getIsVerified() ? pht('Verified') : pht('Unverified'); } else { $status = pht('No Email Address'); } $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Email')) ->setValue($status)); $form->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, pht('Re-send "Welcome to Phabricator" email.'), false)); } $form->appendChild($this->getRoleInstructions()); if (!$user->getID()) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Role')) ->setName('role') ->setValue('user') ->setOptions( array( 'user' => pht('Normal User'), 'agent' => pht('System Agent'), )) ->setCaption( pht('You can create a "system agent" account for bots, '. 'scripts, etc.'))) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, pht('Send "Welcome to Phabricator" email.'), $welcome_checked)); } else { $roles = array(); if ($user->getIsSystemAgent()) { $roles[] = pht('System Agent'); } if ($user->getIsAdmin()) { $roles[] = pht('Admin'); } if ($user->getIsDisabled()) { $roles[] = pht('Disabled'); } if (!$user->getIsApproved()) { $roles[] = pht('Not Approved'); } if (!$roles) { $roles[] = pht('Normal User'); } $roles = implode(', ', $roles); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Roles')) ->setValue($roles)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save'))); if ($user->getID()) { $title = pht('Edit User'); } else { $title = pht('Create New User'); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return array($form_box); } private function processRoleRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $is_self = ($user->getID() == $admin->getID()); $errors = array(); if ($request->isFormPost()) { - $log_template = PhabricatorUserLog::newLog( + $log_template = PhabricatorUserLog::initializeNewLog( $admin, - $user, + $user->getPHID(), null); $logs = array(); if ($is_self) { $errors[] = pht("You can not edit your own role."); } else { $new_admin = (bool)$request->getBool('is_admin'); $old_admin = (bool)$user->getIsAdmin(); if ($new_admin != $old_admin) { id(new PhabricatorUserEditor()) ->setActor($admin) ->makeAdminUser($user, $new_admin); } $new_disabled = (bool)$request->getBool('is_disabled'); $old_disabled = (bool)$user->getIsDisabled(); if ($new_disabled != $old_disabled) { id(new PhabricatorUserEditor()) ->setActor($admin) ->disableUser($user, $new_disabled); } } if (!$errors) { return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('saved', 'true')); } } $form = id(new AphrontFormView()) ->setUser($admin) ->setAction($request->getRequestURI()->alter('saved', null)); if ($is_self) { $inst = pht('NOTE: You can not edit your own role.'); $form->appendChild( phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); } $form ->appendChild($this->getRoleInstructions()) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_admin', 1, pht('Administrator'), $user->getIsAdmin()) ->setDisabled($is_self)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_disabled', 1, pht('Disabled'), $user->getIsDisabled()) ->setDisabled($is_self)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_agent', 1, pht('System Agent (Bot/Script User)'), $user->getIsSystemAgent()) ->setDisabled(true)); if (!$is_self) { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Edit Role'))); } $title = pht('Edit Role'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return array($form_box); } private function processCertificateRequest($user) { $request = $this->getRequest(); $admin = $request->getUser(); $inst = pht('You can use this certificate '. 'to write scripts or bots which interface with Phabricator over '. 'Conduit.'); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild( phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); if ($user->getIsSystemAgent()) { $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Certificate')) ->setValue($user->getConduitCertificate())); } else { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Certificate')) ->setValue( pht('You may only view the certificates of System Agents.'))); } $title = pht('Conduit Certificate'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return array($form_box); } private function processRenameRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $e_username = true; $username = $user->getUsername(); $errors = array(); if ($request->isFormPost()) { $username = $request->getStr('username'); if (!strlen($username)) { $e_username = pht('Required'); $errors[] = pht('New username is required.'); } else if ($username == $user->getUsername()) { $e_username = pht('Invalid'); $errors[] = pht('New username must be different from old username.'); } else if (!PhabricatorUser::validateUsername($username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } if (!$errors) { try { id(new PhabricatorUserEditor()) ->setActor($admin) ->changeUsername($user, $username); return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('saved', true)); } catch (AphrontQueryDuplicateKeyException $ex) { $e_username = pht('Not Unique'); $errors[] = pht('Another user already has that username.'); } } } $inst1 = pht('Be careful when renaming users!'); $inst2 = pht('The old username will no longer be tied to the user, so '. 'anything which uses it (like old commit messages) will no longer '. 'associate correctly. And if you give a user a username which some '. 'other user used to have, username lookups will begin returning '. 'the wrong user.'); $inst3 = pht('It is generally safe to rename newly created users (and '. 'test users and so on), but less safe to rename established users '. 'and unsafe to reissue a username.'); $inst4 = pht('Users who rely on password auth will need to reset their '. 'passwordafter their username is changed (their username is part '. 'of the salt in the password hash). They will receive an email '. 'with instructions on how to do this.'); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild(hsprintf( '

'. '%s '. '%s'. '

'. '

'. '%s'. '

'. '

'. '%s'. '

', $inst1, $inst2, $inst3, $inst4)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Old Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('New Username')) ->setValue($username) ->setName('username') ->setError($e_username)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Change Username'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Change Username')) ->setFormErrors($errors) ->setForm($form); return array($form_box); } private function processDeleteRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $far1 = pht('As you stare into the gaping maw of the abyss, something '. 'hold you back.'); $far2 = pht('You can not delete your own account.'); if ($user->getPHID() == $admin->getPHID()) { $error = new AphrontErrorView(); $error->setTitle(pht('You Shall Journey No Farther')); $error->appendChild(hsprintf( '

%s

%s

', $far1, $far2)); return $error; } $e_username = true; $username = null; $errors = array(); if ($request->isFormPost()) { $username = $request->getStr('username'); if (!strlen($username)) { $e_username = pht('Required'); $errors[] = pht('You must type the username to confirm deletion.'); } else if ($username != $user->getUsername()) { $e_username = pht('Invalid'); $errors[] = pht('You must type the username correctly.'); } if (!$errors) { id(new PhabricatorUserEditor()) ->setActor($admin) ->deleteUser($user); return id(new AphrontRedirectResponse())->setURI('/people/'); } } $str1 = pht('Be careful when deleting users!'); $str2 = pht('If this user interacted with anything, it is generally '. 'better to disable them, not delete them. If you delete them, it will '. 'no longer be possible to search for their objects, for example, '. 'and you will lose other information about their history. Disabling '. 'them instead will prevent them from logging in but not destroy '. 'any of their data.'); $str3 = pht('It is generally safe to delete newly created users (and '. 'test users and so on), but less safe to delete established users. '. 'If possible, disable them instead.'); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild(hsprintf( '

'. '%s %s'. '

'. '

'. '%s'. '

', $str1, $str2, $str3)) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Confirm')) ->setValue($username) ->setName('username') ->setCaption(pht("Type the username again to confirm deletion.")) ->setError($e_username)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Delete User'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Delete User')) ->setFormErrors($errors) ->setForm($form); return array($form_box); } private function getRoleInstructions() { $roles_link = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'article/User_Guide_Account_Roles.html'), 'target' => '_blank', ), pht('User Guide: Account Roles')); return phutil_tag( 'p', array('class' => 'aphront-form-instructions'), pht('For a detailed explanation of account roles, see %s.', $roles_link)); } private function processSetAccountPicture(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $profile = $user->loadUserProfile(); if (!$profile->getID()) { $profile->setTitle(''); $profile->setBlurb(''); } $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_image = null; $errors = array(); if ($request->isFormPost()) { $default_image = $request->getExists('default_image'); if ($default_image) { $profile->setProfileImagePHID(null); $user->setProfileImagePHID(null); } else if ($request->getFileExists('image')) { $file = null; $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $admin->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); // Generate the large picture for the profile page. $large_xformed = $xformer->executeProfileTransform( $file, $width = 280, $min_height = 140, $max_height = 420); $profile->setProfileImagePHID($large_xformed->getPHID()); // Generate the small picture for comments, etc. $small_xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); $user->setProfileImagePHID($small_xformed->getPHID()); } else { $e_image = pht('Not Supported'); $errors[] = pht('This server only supports these image formats:'). ' ' .implode(', ', $supported_formats); } } if (!$errors) { $user->save(); $profile->save(); $response = id(new AphrontRedirectResponse()) ->setURI('/people/edit/'.$user->getID().'/picture/'); return $response; } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle(pht('Form Errors')); $error_view->setErrors($errors); } else { if ($request->getStr('saved')) { $error_view = new AphrontErrorView(); $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $error_view->setTitle(pht('Changes Saved')); $error_view->appendChild( phutil_tag('p', array(), pht('Your changes have been saved.'))); $error_view = $error_view->render(); } } $img_src = $user->loadProfileImageURI(); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Profile Image')) ->setValue( phutil_tag( 'img', array( 'src' => $img_src, )))) ->appendChild( id(new AphrontFormImageControl()) ->setLabel(pht('Change Image')) ->setName('image') ->setError($e_image) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))); $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton('/people/edit/'.$user->getID().'/')); $panel = new AphrontPanelView(); $panel->setHeader(pht('Set Profile Picture')); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->setNoBackground(); $panel->appendChild($form); return array($error_view, $panel); } } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index af9139f039..6d982143c6 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -1,578 +1,578 @@ getID()) { throw new Exception("User has already been created!"); } if ($email->getID()) { throw new Exception("Email has already been created!"); } if (!PhabricatorUser::validateUsername($user->getUsername())) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } // Always set a new user's email address to primary. $email->setIsPrimary(1); // If the primary address is already verified, also set the verified flag // on the user themselves. if ($email->getIsVerified()) { $user->setIsEmailVerified(1); } $this->willAddEmail($email); $user->openTransaction(); try { $user->save(); $email->setUserPHID($user->getPHID()); $email->save(); } catch (AphrontQueryDuplicateKeyException $ex) { // We might have written the user but failed to write the email; if // so, erase the IDs we attached. $user->setID(null); $user->setPHID(null); $user->killTransaction(); throw $ex; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_CREATE); $log->setNewValue($email->getAddress()); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function updateUser( PhabricatorUser $user, PhabricatorUserEmail $email = null) { if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->save(); if ($email) { $email->save(); } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EDIT); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function changePassword( PhabricatorUser $user, PhutilOpaqueEnvelope $envelope) { if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->reload(); $user->setPassword($envelope); $user->save(); - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_PASSWORD); $log->save(); $user->saveTransaction(); } /** * @task edit */ public function changeUsername(PhabricatorUser $user, $username) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if (!PhabricatorUser::validateUsername($username)) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } $old_username = $user->getUsername(); $user->openTransaction(); $user->reload(); $user->setUsername($username); try { $user->save(); } catch (AphrontQueryDuplicateKeyException $ex) { $user->setUsername($old_username); $user->killTransaction(); throw $ex; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_USERNAME); $log->setOldValue($old_username); $log->setNewValue($username); $log->save(); $user->saveTransaction(); $user->sendUsernameChangeEmail($actor, $old_username); } /* -( Editing Roles )------------------------------------------------------ */ /** * @task role */ public function makeAdminUser(PhabricatorUser $user, $admin) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsAdmin() == $admin) { $user->endWriteLocking(); $user->killTransaction(); return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_ADMIN); $log->setOldValue($user->getIsAdmin()); $log->setNewValue($admin); - $user->setIsAdmin($admin); + $user->setIsAdmin((int)$admin); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsSystemAgent() == $system_agent) { $user->endWriteLocking(); $user->killTransaction(); return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_SYSTEM_AGENT); $log->setOldValue($user->getIsSystemAgent()); $log->setNewValue($system_agent); $user->setIsSystemAgent($system_agent); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function disableUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsDisabled() == $disable) { $user->endWriteLocking(); $user->killTransaction(); return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_DISABLE); $log->setOldValue($user->getIsDisabled()); $log->setNewValue($disable); - $user->setIsDisabled($disable); + $user->setIsDisabled((int)$disable); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function approveUser(PhabricatorUser $user, $approve) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsApproved() == $approve) { $user->endWriteLocking(); $user->killTransaction(); return $this; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_APPROVE); $log->setOldValue($user->getIsApproved()); $log->setNewValue($approve); $user->setIsApproved($approve); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function deleteUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if ($actor->getPHID() == $user->getPHID()) { throw new Exception("You can not delete yourself!"); } $user->openTransaction(); $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($externals as $external) { $external->delete(); } $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($prefs as $pref) { $pref->delete(); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($keys as $key) { $key->delete(); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($emails as $email) { $email->delete(); } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_DELETE); $log->save(); $user->delete(); $user->saveTransaction(); return $this; } /* -( Adding, Removing and Changing Email )-------------------------------- */ /** * @task email */ public function addEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if ($email->getID()) { throw new Exception("Email has already been created!"); } // Use changePrimaryEmail() to change primary email. $email->setIsPrimary(0); $email->setUserPHID($user->getPHID()); $this->willAddEmail($email); $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); try { $email->save(); } catch (AphrontQueryDuplicateKeyException $ex) { $user->endWriteLocking(); $user->killTransaction(); throw $ex; } - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_ADD); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task email */ public function removeEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if (!$email->getID()) { throw new Exception("Email has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getIsPrimary()) { throw new Exception("Can't remove primary email!"); } if ($email->getUserPHID() != $user->getPHID()) { throw new Exception("Email not owned by user!"); } $email->delete(); - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initializeNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_REMOVE); $log->setOldValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task email */ public function changePrimaryEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if (!$email->getID()) { throw new Exception("Email has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getUserPHID() != $user->getPHID()) { throw new Exception("User does not own email!"); } if ($email->getIsPrimary()) { throw new Exception("Email is already primary!"); } if (!$email->getIsVerified()) { throw new Exception("Email is not verified!"); } $old_primary = $user->loadPrimaryEmail(); if ($old_primary) { $old_primary->setIsPrimary(0); $old_primary->save(); } $email->setIsPrimary(1); $email->save(); - $log = PhabricatorUserLog::newLog( + $log = PhabricatorUserLog::initialieNewLog( $actor, - $user, + $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_PRIMARY); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); if ($old_primary) { $old_primary->sendOldPrimaryEmail($user, $email); } $email->sendNewPrimaryEmail($user); return $this; } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function willAddEmail(PhabricatorUserEmail $email) { // Hard check before write to prevent creation of disallowed email // addresses. Normally, the application does checks and raises more // user friendly errors for us, but we omit the courtesy checks on some // pathways like administrative scripts for simplicity. if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) { throw new Exception(PhabricatorUserEmail::describeAllowedAddresses()); } } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index aecd0e4357..32e00c0624 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,910 +1,910 @@ 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; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_PARTIAL_OBJECTS => 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(mt_rand())); $hash = $this->hashPassword($envelope); $this->setPasswordHash($hash); } return $this; } // To satisfy PhutilPerson. public function getSex() { return $this->sex; } 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()); } $result = parent::save(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($this->getPHID()); return $result; } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } public function comparePassword(PhutilOpaqueEnvelope $envelope) { if (!strlen($envelope->openEnvelope())) { return false; } if (!strlen($this->getPasswordHash())) { return false; } $password_hash = $this->hashPassword($envelope); return ($password_hash === $this->getPasswordHash()); } private function hashPassword(PhutilOpaqueEnvelope $envelope) { $hash = $this->getUsername(). $envelope->openEnvelope(). $this->getPHID(). $this->getPasswordSalt(); for ($ii = 0; $ii < 1000; $ii++) { $hash = md5($hash); } return $hash; } 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) { if (!$this->getPHID()) { return true; } $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) { $time_block = floor($epoch / $frequency); $vec = $this->getPHID().$this->getPasswordHash().$key.$time_block; return substr(PhabricatorHash::digest($vec), 0, $len); } /** * Issue a new session key to this user. Phabricator supports different * types of sessions (like "web" and "conduit") and each session type may * have multiple concurrent sessions (this allows a user to be logged in on * multiple browsers at the same time, for instance). * * Note that this method is transport-agnostic and does not set cookies or * issue other types of tokens, it ONLY generates a new session key. * * You can configure the maximum number of concurrent sessions for various * session types in the Phabricator configuration. * * @param string Session type, like "web". * @return string Newly generated session key. */ public function establishSession($session_type) { $conn_w = $this->establishConnection('w'); if (strpos($session_type, '-') !== false) { throw new Exception("Session type must not contain hyphen ('-')!"); } // We allow multiple sessions of the same type, so when a caller requests // a new session of type "web", we give them the first available session in // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none // of these sessions is available, we overwrite the oldest session and // reissue a new one in its place. $session_limit = 1; switch ($session_type) { case 'web': $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); break; case 'conduit': $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); break; default: throw new Exception("Unknown session type '{$session_type}'!"); } $session_limit = (int)$session_limit; if ($session_limit <= 0) { throw new Exception( "Session limit for '{$session_type}' must be at least 1!"); } // NOTE: Session establishment is sensitive to race conditions, as when // piping `arc` to `arc`: // // arc export ... | arc paste ... // // To avoid this, we overwrite an old session only if it hasn't been // re-established since we read it. // Consume entropy to generate a new session key, forestalling the eventual // heat death of the universe. $session_key = Filesystem::readRandomCharacters(40); // Load all the currently active sessions. $sessions = queryfx_all( $conn_w, 'SELECT type, sessionKey, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>', PhabricatorUser::SESSION_TABLE, $this->getPHID(), $session_type.'-'); $sessions = ipull($sessions, null, 'type'); $sessions = isort($sessions, 'sessionStart'); $existing_sessions = array_keys($sessions); // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $retries = 0; while (true) { // Choose which 'type' we'll actually establish, i.e. what number we're // going to append to the basic session type. To do this, just check all // the numbers sequentially until we find an available session. $establish_type = null; for ($ii = 1; $ii <= $session_limit; $ii++) { $try_type = $session_type.'-'.$ii; if (!in_array($try_type, $existing_sessions)) { $establish_type = $try_type; $expect_key = PhabricatorHash::digest($session_key); $existing_sessions[] = $try_type; // Ensure the row exists so we can issue an update below. We don't // care if we race here or not. queryfx( $conn_w, 'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart) VALUES (%s, %s, %s, 0)', self::SESSION_TABLE, $this->getPHID(), $establish_type, PhabricatorHash::digest($session_key)); break; } } // If we didn't find an available session, choose the oldest session and // overwrite it. if (!$establish_type) { $oldest = reset($sessions); $establish_type = $oldest['type']; $expect_key = $oldest['sessionKey']; } // This is so that we'll only overwrite the session if it hasn't been // refreshed since we read it. If it has, the session key will be // different and we know we're racing other processes. Whichever one // won gets the session, we go back and try again. queryfx( $conn_w, 'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP() WHERE userPHID = %s AND type = %s AND sessionKey = %s', self::SESSION_TABLE, PhabricatorHash::digest($session_key), $this->getPHID(), $establish_type, $expect_key); if ($conn_w->getAffectedRows()) { // The update worked, so the session is valid. break; } else { // We know this just got grabbed, so don't try it again. unset($sessions[$establish_type]); } if (++$retries > $session_limit) { throw new Exception("Failed to establish a session!"); } } - $log = PhabricatorUserLog::newLog( - $this, + $log = PhabricatorUserLog::initializeNewLog( $this, + $this->getPHID(), PhabricatorUserLog::ACTION_LOGIN); $log->setDetails( array( 'session_type' => $session_type, 'session_issued' => $establish_type, )); $log->setSession($session_key); $log->save(); return $session_key; } public function destroySession($session_key) { $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s', self::SESSION_TABLE, $this->getPHID(), PhabricatorHash::digest($session_key)); } private function generateEmailToken( PhabricatorUserEmail $email, $offset = 0) { $key = implode( '-', array( PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), $this->getPHID(), $email->getVerificationCode(), )); return $this->generateToken( time() + ($offset * self::EMAIL_CYCLE_FREQUENCY), self::EMAIL_CYCLE_FREQUENCY, $key, self::EMAIL_TOKEN_LENGTH); } public function validateEmailToken( PhabricatorUserEmail $email, $token) { for ($ii = -1; $ii <= 1; $ii++) { $valid = $this->generateEmailToken($email, $ii); if ($token == $valid) { return true; } } return false; } public function getEmailLoginURI(PhabricatorUserEmail $email = null) { if (!$email) { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception("User has no primary email!"); } } $token = $this->generateEmailToken($email); $uri = '/login/etoken/'.$token.'/'; try { $uri = PhabricatorEnv::getProductionURI($uri); } catch (Exception $ex) { // If a user runs `bin/auth recover` before configuring the base URI, // just show the path. We don't have any way to figure out the domain. // See T4132. } $uri = new PhutilURI($uri); return $uri->alter('email', $email->getAddress()); } 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 ($editor) { return strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); } } private static function tokenizeName($name) { if (function_exists('mb_strtolower')) { $name = mb_strtolower($name, 'UTF-8'); } else { $name = strtolower($name); } $name = trim($name); if (!strlen($name)) { return array(); } return preg_split('/\s+/', $name); } /** * 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() { $tokens = array_merge( self::tokenizeName($this->getRealName()), self::tokenizeName($this->getUserName())); $tokens = array_unique($tokens); $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $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('/'); $uri = $this->getEmailLoginURI(); $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()) { $uri = $this->getEmailLoginURI(); $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_-]$/', $username); } public static function getDefaultProfileImageURI() { return celerity_get_resource_uri('/rsrc/image/avatar.png'); } public function attachStatus(PhabricatorUserStatus $status) { $this->status = $status; return $this; } public function getStatus() { $this->assertAttached($this->status); return $this->status; } public function hasStatus() { return $this->status !== self::ATTACHABLE; } public function attachProfileImageURI($uri) { $this->profileImage = $uri; return $this; } public function loadProfileImageURI() { if ($this->profileImage) { 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(); } } if (!$this->profileImage) { $this->profileImage = self::getDefaultProfileImageURI(); } return $this->profileImage; } public function getFullName() { return $this->getUsername().' ('.$this->getRealName().')'; } 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()); } /* -( 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: 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; } } diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index a0b93fb464..311a4824d1 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -1,104 +1,97 @@ setActorPHID($actor->getPHID()); } - if ($user) { - $log->setUserPHID($user->getPHID()); - } else { - $log->setUserPHID(''); - } - - if ($action) { - $log->setAction($action); - } + $log->setUserPHID((string)$object_phid); + $log->setAction($action); return $log; } public static function loadRecentEventsFromThisIP($action, $timespan) { return id(new PhabricatorUserLog())->loadAllWhere( 'action = %s AND remoteAddr = %s AND dateCreated > %d ORDER BY dateCreated DESC', $action, idx($_SERVER, 'REMOTE_ADDR'), time() - $timespan); } public function save() { if (!$this->remoteAddr) { $this->remoteAddr = idx($_SERVER, 'REMOTE_ADDR', ''); } if (!$this->session) { $this->setSession(idx($_COOKIE, 'phsid')); } $this->details['host'] = php_uname('n'); $this->details['user_agent'] = AphrontRequest::getHTTPHeader('User-Agent'); return parent::save(); } public function setSession($session) { // Store the hash of the session, not the actual session key, so that // seeing the logs doesn't compromise all the sessions which appear in // them. This just prevents casual leaks, like in a screenshot. if (strlen($session)) { $this->session = PhabricatorHash::digest($session); } return $this; } public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } }