diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f86b3a1d2d..f031f7deca 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2112,6 +2112,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', 'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php', 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', + 'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php', 'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php', 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php', @@ -7377,6 +7378,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthSessionInfo' => 'Phobject', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index dfc855f315..a6127394fd 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -84,6 +84,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { => 'PhabricatorAuthSSHKeyDeactivateController', 'view/(?P\d+)/' => 'PhabricatorAuthSSHKeyViewController', ), + 'password/' => 'PhabricatorAuthSetPasswordController', ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index 534bda3f35..9f74d50765 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -139,8 +139,7 @@ final class PhabricatorAuthOneTimeLoginController ->save(); unset($unguarded); - $username = $target_user->getUsername(); - $panel_uri = "/settings/user/{$username}/page/password/"; + $panel_uri = '/auth/password/'; $next = (string)id(new PhutilURI($panel_uri)) ->setQueryParams( diff --git a/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php b/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php new file mode 100644 index 0000000000..0db23e52a7 --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php @@ -0,0 +1,155 @@ +getViewer(); + + if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { + return new Aphront404Response(); + } + + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( + $viewer, + $request, + '/'); + + $key = $request->getStr('key'); + $password_type = PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE; + if (!$key) { + return new Aphront404Response(); + } + + $auth_token = id(new PhabricatorAuthTemporaryTokenQuery()) + ->setViewer($viewer) + ->withTokenResources(array($viewer->getPHID())) + ->withTokenTypes(array($password_type)) + ->withTokenCodes(array(PhabricatorHash::weakDigest($key))) + ->withExpired(false) + ->executeOne(); + if (!$auth_token) { + return new Aphront404Response(); + } + + $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); + $min_len = (int)$min_len; + + $e_password = true; + $e_confirm = true; + $errors = array(); + if ($request->isFormPost()) { + $password = $request->getStr('password'); + $confirm = $request->getStr('confirm'); + + $e_password = null; + $e_confirm = null; + + if (!strlen($password)) { + $errors[] = pht('You must choose a password or skip this step.'); + $e_password = pht('Required'); + } else if (strlen($password) < $min_len) { + $errors[] = pht( + 'The selected password is too short. Passwords must be a minimum '. + 'of %s characters.', + new PhutilNumber($min_len)); + $e_password = pht('Too Short'); + } else if (!strlen($confirm)) { + $errors[] = pht('You must confirm the selecetd password.'); + $e_confirm = pht('Required'); + } else if ($password !== $confirm) { + $errors[] = pht('The password and confirmation do not match.'); + $e_password = pht('Invalid'); + $e_confirm = pht('Invalid'); + } else if (PhabricatorCommonPasswords::isCommonPassword($password)) { + $e_password = pht('Very Weak'); + $errors[] = pht( + 'The selected password is very weak: it is one of the most common '. + 'passwords in use. Choose a stronger password.'); + } + + if (!$errors) { + $envelope = new PhutilOpaqueEnvelope($password); + + // 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(); + + id(new PhabricatorUserEditor()) + ->setActor($viewer) + ->changePassword($viewer, $envelope); + + unset($unguarded); + + // Destroy the token. + $auth_token->delete(); + + return id(new AphrontRedirectResponse())->setURI('/'); + } + } + + $len_caption = null; + if ($min_len) { + $len_caption = pht('Minimum password length: %d characters.', $min_len); + } + + if ($viewer->hasPassword()) { + $title = pht('Reset Password'); + $crumb = pht('Reset Password'); + $submit = pht('Reset Password'); + } else { + $title = pht('Set Password'); + $crumb = pht('Set Password'); + $submit = pht('Set Account Password'); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->addHiddenInput('key', $key) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setDisableAutocomplete(true) + ->setLabel(pht('New Password')) + ->setError($e_password) + ->setName('password')) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setDisableAutocomplete(true) + ->setLabel(pht('Confirm Password')) + ->setCaption($len_caption) + ->setError($e_confirm) + ->setName('confirm')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/', pht('Skip This Step')) + ->setValue($submit)); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) + ->setForm($form); + + $main_view = id(new PHUITwoColumnView()) + ->setFooter($form_box); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($crumb) + ->setBorder(true); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($main_view); + } +} diff --git a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php index 4ae007fc0c..59d8476c87 100644 --- a/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php +++ b/src/applications/calendar/notifications/PhabricatorCalendarNotificationEngine.php @@ -100,10 +100,11 @@ final class PhabricatorCalendarNotificationEngine } $notifiable_phids[] = $invitee->getInviteePHID(); } - if (!$notifiable_phids) { + if ($notifiable_phids) { + $attendee_map[$key] = array_fuse($notifiable_phids); + } else { unset($events[$key]); } - $attendee_map[$key] = array_fuse($notifiable_phids); } if (!$attendee_map) { // None of the events have any notifiable attendees, so there is no diff --git a/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php index a76f53a362..c4622a902c 100644 --- a/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php +++ b/src/applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php @@ -65,6 +65,16 @@ final class HarbormasterQueryBuildsConduitAPIMethod $fields = idx($build_data, 'fields', array()); unset($build_data['fields']); unset($build_data['attachments']); + + // To retain backward compatibility, remove newer keys from the + // result array. + $fields['buildStatus'] = array_select_keys( + $fields['buildStatus'], + array( + 'value', + 'name', + )); + $data[] = array_mergev(array($build_data, $querybuilds, $fields)); } diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php index a2cdba5b63..b0e3105d78 100644 --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -55,67 +55,28 @@ final class HarbormasterBuildStatus extends Phobject { * @return string Human-readable name. */ public static function getBuildStatusName($status) { - $map = self::getBuildStatusMap(); - return idx($map, $status, pht('Unknown ("%s")', $status)); + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'name', pht('Unknown ("%s")', $status)); } public static function getBuildStatusMap() { - return array( - self::STATUS_INACTIVE => pht('Inactive'), - self::STATUS_PENDING => pht('Pending'), - self::STATUS_BUILDING => pht('Building'), - self::STATUS_PASSED => pht('Passed'), - self::STATUS_FAILED => pht('Failed'), - self::STATUS_ABORTED => pht('Aborted'), - self::STATUS_ERROR => pht('Unexpected Error'), - self::STATUS_PAUSED => pht('Paused'), - self::STATUS_DEADLOCKED => pht('Deadlocked'), - ); + $specs = self::getBuildStatusSpecMap(); + return ipull($specs, 'name'); } public static function getBuildStatusIcon($status) { - switch ($status) { - case self::STATUS_INACTIVE: - case self::STATUS_PENDING: - return PHUIStatusItemView::ICON_OPEN; - case self::STATUS_BUILDING: - return PHUIStatusItemView::ICON_RIGHT; - case self::STATUS_PASSED: - return PHUIStatusItemView::ICON_ACCEPT; - case self::STATUS_FAILED: - return PHUIStatusItemView::ICON_REJECT; - case self::STATUS_ABORTED: - return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_ERROR: - return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_PAUSED: - return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_DEADLOCKED: - return PHUIStatusItemView::ICON_WARNING; - default: - return PHUIStatusItemView::ICON_QUESTION; - } + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'icon', 'fa-question-circle'); } public static function getBuildStatusColor($status) { - switch ($status) { - case self::STATUS_INACTIVE: - return 'dark'; - case self::STATUS_PENDING: - case self::STATUS_BUILDING: - return 'blue'; - case self::STATUS_PASSED: - return 'green'; - case self::STATUS_FAILED: - case self::STATUS_ABORTED: - case self::STATUS_ERROR: - case self::STATUS_DEADLOCKED: - return 'red'; - case self::STATUS_PAUSED: - return 'dark'; - default: - return 'bluegrey'; - } + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'color', 'bluegrey'); + } + + public static function getBuildStatusANSIColor($status) { + $spec = self::getBuildStatusSpec($status); + return idx($spec, 'color.ansi', 'magenta'); } public static function getWaitingStatusConstants() { @@ -142,4 +103,67 @@ final class HarbormasterBuildStatus extends Phobject { ); } + private static function getBuildStatusSpecMap() { + return array( + self::STATUS_INACTIVE => array( + 'name' => pht('Inactive'), + 'icon' => 'fa-circle-o', + 'color' => 'dark', + 'color.ansi' => 'yellow', + ), + self::STATUS_PENDING => array( + 'name' => pht('Pending'), + 'icon' => 'fa-circle-o', + 'color' => 'blue', + 'color.ansi' => 'yellow', + ), + self::STATUS_BUILDING => array( + 'name' => pht('Building'), + 'icon' => 'fa-chevron-circle-right', + 'color' => 'blue', + 'color.ansi' => 'yellow', + ), + self::STATUS_PASSED => array( + 'name' => pht('Passed'), + 'icon' => 'fa-check-circle', + 'color' => 'green', + 'color.ansi' => 'green', + ), + self::STATUS_FAILED => array( + 'name' => pht('Failed'), + 'icon' => 'fa-times-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + self::STATUS_ABORTED => array( + 'name' => pht('Aborted'), + 'icon' => 'fa-minus-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + self::STATUS_ERROR => array( + 'name' => pht('Unexpected Error'), + 'icon' => 'fa-minus-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + self::STATUS_PAUSED => array( + 'name' => pht('Paused'), + 'icon' => 'fa-minus-circle', + 'color' => 'dark', + 'color.ansi' => 'yellow', + ), + self::STATUS_DEADLOCKED => array( + 'name' => pht('Deadlocked'), + 'icon' => 'fa-exclamation-circle', + 'color' => 'red', + 'color.ansi' => 'red', + ), + ); + } + + private static function getBuildStatusSpec($status) { + return idx(self::getBuildStatusSpecMap(), $status, array()); + } + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 958eaa1f2b..92d4293913 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -435,6 +435,8 @@ final class HarbormasterBuild extends HarbormasterDAO 'buildStatus' => array( 'value' => $status, 'name' => HarbormasterBuildStatus::getBuildStatusName($status), + 'color.ansi' => + HarbormasterBuildStatus::getBuildStatusANSIColor($status), ), 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null), 'name' => $this->getName(), diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 1745154826..30aa3d81ef 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -262,6 +262,10 @@ final class PhabricatorUser PhabricatorPeopleUserPHIDType::TYPECONST); } + public function hasPassword() { + return (bool)strlen($this->passwordHash); + } + public function setPassword(PhutilOpaqueEnvelope $envelope) { if (!$this->getPHID()) { throw new Exception( diff --git a/src/applications/project/controller/PhabricatorProjectViewController.php b/src/applications/project/controller/PhabricatorProjectViewController.php index 7d5dc37e0b..2e53fe7276 100644 --- a/src/applications/project/controller/PhabricatorProjectViewController.php +++ b/src/applications/project/controller/PhabricatorProjectViewController.php @@ -20,6 +20,14 @@ final class PhabricatorProjectViewController $engine = $this->getProfileMenuEngine(); $default = $engine->getDefaultItem(); + // If defaults are broken somehow, serve the manage page. See T13033 for + // discussion. + if ($default) { + $default_key = $default->getBuiltinKey(); + } else { + $default_key = PhabricatorProject::ITEM_MANAGE; + } + switch ($default->getBuiltinKey()) { case PhabricatorProject::ITEM_WORKBOARD: $controller_object = new PhabricatorProjectBoardViewController(); @@ -27,6 +35,9 @@ final class PhabricatorProjectViewController case PhabricatorProject::ITEM_PROFILE: $controller_object = new PhabricatorProjectProfileController(); break; + case PhabricatorProject::ITEM_MANAGE: + $controller_object = new PhabricatorProjectManageController(); + break; default: return $engine->buildResponse(); } diff --git a/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php index 96619b8ec5..b779f4be90 100644 --- a/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php @@ -13,6 +13,11 @@ final class PhabricatorProjectDetailsProfileMenuItem return pht('Project Details'); } + public function canHideMenuItem( + PhabricatorProfileMenuItemConfiguration $config) { + return false; + } + public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return true; diff --git a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php index ddb59ec095..1bd7e796dc 100644 --- a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php @@ -18,6 +18,11 @@ final class PhabricatorProjectManageProfileMenuItem return false; } + public function canMakeDefault( + PhabricatorProfileMenuItemConfiguration $config) { + return true; + } + public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $name = $config->getMenuItemProperty('name'); diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php index c1250fca23..3807139fe3 100644 --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -35,23 +35,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { $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'); - $password_type = PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE; - - $token = null; - if ($key) { - $token = id(new PhabricatorAuthTemporaryTokenQuery()) - ->setViewer($user) - ->withTokenResources(array($user->getPHID())) - ->withTokenTypes(array($password_type)) - ->withTokenCodes(array(PhabricatorHash::weakDigest($key))) - ->withExpired(false) - ->executeOne(); - } + // NOTE: Users can also change passwords through the separate "set/reset" + // interface which is reached by logging in with a one-time token after + // registration or password reset. If this flow changes, that flow may + // also need to change. $e_old = true; $e_new = true; @@ -59,12 +46,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { $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'); - } + $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'); @@ -98,16 +83,7 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { 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'); - } + $next = $this->getPanelURI('?saved=true'); id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $user, @@ -125,19 +101,15 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { } catch (PhabricatorPasswordHasherUnavailableException $ex) { $can_upgrade = false; - // Only show this stuff if we aren't on the reset workflow. We can - // do resets regardless of the old hasher's availability. - if (!$token) { - $errors[] = pht( - 'Your password is currently hashed using an algorithm which is '. - 'no longer available on this install.'); - $errors[] = pht( - 'Because the algorithm implementation is missing, your password '. - 'can not be used or updated.'); - $errors[] = pht( - 'To set a new password, request a password reset link from the '. - 'login screen and then follow the instructions.'); - } + $errors[] = pht( + 'Your password is currently hashed using an algorithm which is '. + 'no longer available on this install.'); + $errors[] = pht( + 'Because the algorithm implementation is missing, your password '. + 'can not be used or updated.'); + $errors[] = pht( + 'To set a new password, request a password reset link from the '. + 'login screen and then follow the instructions.'); } if ($can_upgrade) { @@ -153,20 +125,13 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { $len_caption = pht('Minimum password length: %d characters.', $min_len); } - $form = new AphrontFormView(); - $form - ->setUser($user) - ->addHiddenInput('key', $key); - - if (!$token) { - $form->appendChild( + $form = id(new AphrontFormView()) + ->setViewer($user) + ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Old Password')) ->setError($e_old) - ->setName('old_pw')); - } - - $form + ->setName('old_pw')) ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true)