diff --git a/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php b/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php index 2075582386..cd97e2fd7f 100644 --- a/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php +++ b/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php @@ -1,118 +1,122 @@ getUser()->getIsMailingList()) { return false; } return true; } public function getPanelKey() { return 'apitokens'; } public function getPanelName() { return pht('Conduit API Tokens'); } + public function getPanelMenuIcon() { + return id(new PhabricatorConduitApplication())->getIcon(); + } + public function getPanelGroupKey() { return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY; } public function isEnabled() { return true; } public function processRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $user = $this->getUser(); $tokens = id(new PhabricatorConduitTokenQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->withExpired(false) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $rows = array(); foreach ($tokens as $token) { $rows[] = array( javelin_tag( 'a', array( 'href' => '/conduit/token/edit/'.$token->getID().'/', 'sigil' => 'workflow', ), $token->getPublicTokenName()), PhabricatorConduitToken::getTokenTypeName($token->getTokenType()), phabricator_datetime($token->getDateCreated(), $viewer), ($token->getExpires() ? phabricator_datetime($token->getExpires(), $viewer) : pht('Never')), javelin_tag( 'a', array( 'class' => 'button small button-grey', 'href' => '/conduit/token/terminate/'.$token->getID().'/', 'sigil' => 'workflow', ), pht('Terminate')), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht("You don't have any active API tokens.")); $table->setHeaders( array( pht('Token'), pht('Type'), pht('Created'), pht('Expires'), null, )); $table->setColumnClasses( array( 'wide pri', '', 'right', 'right', 'action', )); $generate_button = id(new PHUIButtonView()) ->setText(pht('Generate Token')) ->setHref('/conduit/token/edit/?objectPHID='.$user->getPHID()) ->setTag('a') ->setWorkflow(true) ->setIcon('fa-plus'); $terminate_button = id(new PHUIButtonView()) ->setText(pht('Terminate Tokens')) ->setHref('/conduit/token/terminate/?objectPHID='.$user->getPHID()) ->setTag('a') ->setWorkflow(true) ->setIcon('fa-exclamation-triangle') ->setColor(PHUIButtonView::RED); $header = id(new PHUIHeaderView()) ->setHeader(pht('Active API Tokens')) ->addActionLink($generate_button) ->addActionLink($terminate_button); $panel = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->appendChild($table); return $panel; } } diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php index 1899302223..789adfbf57 100644 --- a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php +++ b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php @@ -1,264 +1,268 @@ getUser()->getIsMailingList()) { return false; } return true; } public function getPanelKey() { return 'vcspassword'; } public function getPanelName() { return pht('VCS Password'); } + public function getPanelMenuIcon() { + return 'fa-code'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } public function isEnabled() { return PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); } public function processRequest(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, '/settings/'); $vcs_type = PhabricatorAuthPassword::PASSWORD_TYPE_VCS; $vcspasswords = id(new PhabricatorAuthPasswordQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->withPasswordTypes(array($vcs_type)) ->withIsRevoked(false) ->execute(); if ($vcspasswords) { $vcspassword = head($vcspasswords); } else { $vcspassword = PhabricatorAuthPassword::initializeNewPassword( $user, $vcs_type); } $panel_uri = $this->getPanelURI('?saved=true'); $errors = array(); $e_password = true; $e_confirm = true; $content_source = PhabricatorContentSource::newFromRequest($request); // NOTE: This test is against $viewer (not $user), so that the error // message below makes sense in the case that the two are different, // and because an admin reusing their own password is bad, while // system agents generally do not have passwords anyway. $engine = id(new PhabricatorAuthPasswordEngine()) ->setViewer($viewer) ->setContentSource($content_source) ->setObject($viewer) ->setPasswordType($vcs_type); if ($request->isFormPost()) { if ($request->getBool('remove')) { if ($vcspassword->getID()) { $vcspassword->delete(); return id(new AphrontRedirectResponse())->setURI($panel_uri); } } $new_password = $request->getStr('password'); $confirm = $request->getStr('confirm'); $envelope = new PhutilOpaqueEnvelope($new_password); $confirm_envelope = new PhutilOpaqueEnvelope($confirm); try { $engine->checkNewPassword($envelope, $confirm_envelope); $e_password = null; $e_confirm = null; } catch (PhabricatorAuthPasswordException $ex) { $errors[] = $ex->getMessage(); $e_password = $ex->getPasswordError(); $e_confirm = $ex->getConfirmError(); } if (!$errors) { $vcspassword ->setPassword($envelope, $user) ->save(); return id(new AphrontRedirectResponse())->setURI($panel_uri); } } $title = pht('Set VCS Password'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht( 'To access repositories hosted by Phabricator over HTTP, you must '. 'set a version control password. This password should be unique.'. "\n\n". "This password applies to all repositories available over ". "HTTP.")); if ($vcspassword->getID()) { $form ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setLabel(pht('Current Password')) ->setDisabled(true) ->setValue('********************')); } else { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Password')) ->setValue(phutil_tag('em', array(), pht('No Password Set')))); } $form ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setName('password') ->setLabel(pht('New VCS Password')) ->setError($e_password)) ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setName('confirm') ->setLabel(pht('Confirm VCS Password')) ->setError($e_confirm)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Change Password'))); if (!$vcspassword->getID()) { $is_serious = PhabricatorEnv::getEnvConfig( 'phabricator.serious-business'); $suggest = Filesystem::readRandomBytes(128); $suggest = preg_replace('([^A-Za-z0-9/!().,;{}^&*%~])', '', $suggest); $suggest = substr($suggest, 0, 20); if ($is_serious) { $form->appendRemarkupInstructions( pht( 'Having trouble coming up with a good password? Try this randomly '. 'generated one, made by a computer:'. "\n\n". "`%s`", $suggest)); } else { $form->appendRemarkupInstructions( pht( 'Having trouble coming up with a good password? Try this '. 'artisanal password, hand made in small batches by our expert '. 'craftspeople: '. "\n\n". "`%s`", $suggest)); } } $hash_envelope = new PhutilOpaqueEnvelope($vcspassword->getPasswordHash()); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Current Algorithm')) ->setValue( PhabricatorPasswordHasher::getCurrentAlgorithmName($hash_envelope))); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Best Available Algorithm')) ->setValue(PhabricatorPasswordHasher::getBestAlgorithmName())); if (strlen($hash_envelope->openEnvelope())) { try { $can_upgrade = PhabricatorPasswordHasher::canUpgradeHash( $hash_envelope); } catch (PhabricatorPasswordHasherUnavailableException $ex) { $can_upgrade = false; $errors[] = pht( 'Your VCS 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.'); $errors[] = pht( 'You can set a new password to replace the old password.'); } if ($can_upgrade) { $errors[] = pht( 'The strength of your stored VCS password hash can be upgraded. '. 'To upgrade, either: use the password to authenticate with a '. 'repository; or change your password.'); } } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form) ->setFormErrors($errors); $remove_form = id(new AphrontFormView()) ->setUser($viewer); if ($vcspassword->getID()) { $remove_form ->addHiddenInput('remove', true) ->appendRemarkupInstructions( pht( 'You can remove your VCS password, which will prevent your '. 'account from accessing repositories.')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Remove Password'))); } else { $remove_form->appendRemarkupInstructions( pht( 'You do not currently have a VCS password set. If you set one, you '. 'can remove it here later.')); } $remove_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Remove VCS Password')) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($remove_form); $saved = null; if ($request->getBool('saved')) { $saved = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Password Updated')) ->appendChild(pht('Your VCS password has been updated.')); } return array( $saved, $object_box, $remove_box, ); } } diff --git a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php index 37e85ab53b..89a1cc0281 100644 --- a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php +++ b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php @@ -1,143 +1,147 @@ getUser(); // TODO: It would be nice to simply disable this panel, but we can't do // viewer-based checks for enabled panels right now. $app_class = 'PhabricatorOAuthServerApplication'; $installed = PhabricatorApplication::isClassInstalledForViewer( $app_class, $viewer); if (!$installed) { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('OAuth Not Available')) ->appendParagraph( pht('You do not have access to OAuth authorizations.')) ->addCancelButton('/settings/'); return id(new AphrontDialogResponse())->setDialog($dialog); } $authorizations = id(new PhabricatorOAuthClientAuthorizationQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->execute(); $authorizations = mpull($authorizations, null, 'getID'); $panel_uri = $this->getPanelURI(); $revoke = $request->getInt('revoke'); if ($revoke) { if (empty($authorizations[$revoke])) { return new Aphront404Response(); } if ($request->isFormPost()) { $authorizations[$revoke]->delete(); return id(new AphrontRedirectResponse())->setURI($panel_uri); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Revoke Authorization?')) ->appendParagraph( pht( 'This application will no longer be able to access Phabricator '. 'on your behalf.')) ->addSubmitButton(pht('Revoke Authorization')) ->addCancelButton($panel_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } $highlight = $request->getInt('id'); $rows = array(); $rowc = array(); foreach ($authorizations as $authorization) { if ($highlight == $authorization->getID()) { $rowc[] = 'highlighted'; } else { $rowc[] = null; } $button = javelin_tag( 'a', array( 'href' => $this->getPanelURI('?revoke='.$authorization->getID()), 'class' => 'small button button-grey', 'sigil' => 'workflow', ), pht('Revoke')); $rows[] = array( phutil_tag( 'a', array( 'href' => $authorization->getClient()->getViewURI(), ), $authorization->getClient()->getName()), $authorization->getScopeString(), phabricator_datetime($authorization->getDateCreated(), $viewer), phabricator_datetime($authorization->getDateModified(), $viewer), $button, ); } $table = new AphrontTableView($rows); $table->setNoDataString( pht("You haven't authorized any OAuth applications.")); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Application'), pht('Scope'), pht('Created'), pht('Updated'), null, )); $table->setColumnClasses( array( 'pri', 'wide', 'right', 'right', 'action', )); $header = id(new PHUIHeaderView()) ->setHeader(pht('OAuth Application Authorizations')); $panel = id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->appendChild($table); return $panel; } } diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php index 46246c3ce5..ded20a8e96 100644 --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -1,238 +1,242 @@ user; } private function isSelf() { $user = $this->getUser(); if (!$user) { return false; } $user_phid = $user->getPHID(); $viewer_phid = $this->getViewer()->getPHID(); return ($viewer_phid == $user_phid); } private function isTemplate() { return ($this->builtinKey !== null); } public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); // Redirect "/panel/XYZ/" to the viewer's personal settings panel. This // was the primary URI before global settings were introduced and allows // generation of viewer-agnostic URIs for email and logged-out users. $panel = $request->getURIData('panel'); if ($panel) { $panel = phutil_escape_uri($panel); $username = $viewer->getUsername(); $panel_uri = "/user/{$username}/page/{$panel}/"; $panel_uri = $this->getApplicationURI($panel_uri); return id(new AphrontRedirectResponse())->setURI($panel_uri); } $username = $request->getURIData('username'); $builtin = $request->getURIData('builtin'); $key = $request->getURIData('pageKey'); if ($builtin) { $this->builtinKey = $builtin; $preferences = id(new PhabricatorUserPreferencesQuery()) ->setViewer($viewer) ->withBuiltinKeys(array($builtin)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$preferences) { $preferences = id(new PhabricatorUserPreferences()) ->attachUser(null) ->setBuiltinKey($builtin); } } else { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withUsernames(array($username)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$user) { return new Aphront404Response(); } $preferences = PhabricatorUserPreferences::loadUserPreferences($user); $this->user = $user; } if (!$preferences) { return new Aphront404Response(); } PhabricatorPolicyFilter::requireCapability( $viewer, $preferences, PhabricatorPolicyCapability::CAN_EDIT); $this->preferences = $preferences; $panels = $this->buildPanels($preferences); $nav = $this->renderSideNav($panels); $key = $nav->selectFilter($key, head($panels)->getPanelKey()); $panel = $panels[$key] ->setController($this) ->setNavigation($nav); $response = $panel->processRequest($request); if (($response instanceof AphrontResponse) || ($response instanceof AphrontResponseProducerInterface)) { return $response; } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($panel->getPanelName()); $crumbs->setBorder(true); if ($this->user) { $header_text = pht('Edit Settings: %s', $user->getUserName()); } else { $header_text = pht('Edit Global Settings'); } $header = id(new PHUIHeaderView()) ->setHeader($header_text); $title = $panel->getPanelName(); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($response); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($view); } private function buildPanels(PhabricatorUserPreferences $preferences) { $viewer = $this->getViewer(); $panels = PhabricatorSettingsPanel::getAllDisplayPanels(); $result = array(); foreach ($panels as $key => $panel) { $panel ->setPreferences($preferences) ->setViewer($viewer); if ($this->user) { $panel->setUser($this->user); } if (!$panel->isEnabled()) { continue; } if ($this->isTemplate()) { if (!$panel->isTemplatePanel()) { continue; } } else { if (!$this->isSelf() && !$panel->isManagementPanel()) { continue; } if ($this->isSelf() && !$panel->isUserPanel()) { continue; } } if (!empty($result[$key])) { throw new Exception(pht( "Two settings panels share the same panel key ('%s'): %s, %s.", $key, get_class($panel), get_class($result[$key]))); } $result[$key] = $panel; } if (!$result) { throw new Exception(pht('No settings panels are available.')); } return $result; } private function renderSideNav(array $panels) { $nav = new AphrontSideNavFilterView(); if ($this->isTemplate()) { $base_uri = 'builtin/'.$this->builtinKey.'/page/'; } else { $user = $this->getUser(); $base_uri = 'user/'.$user->getUsername().'/page/'; } $nav->setBaseURI(new PhutilURI($this->getApplicationURI($base_uri))); $group_key = null; foreach ($panels as $panel) { if ($panel->getPanelGroupKey() != $group_key) { $group_key = $panel->getPanelGroupKey(); $group = $panel->getPanelGroup(); $panel_name = $group->getPanelGroupName(); if ($panel_name) { $nav->addLabel($panel_name); } } - $nav->addFilter($panel->getPanelKey(), $panel->getPanelName()); + $nav->addFilter( + $panel->getPanelKey(), + $panel->getPanelName(), + null, + $panel->getPanelMenuIcon()); } return $nav; } public function buildApplicationMenu() { if ($this->preferences) { $panels = $this->buildPanels($this->preferences); return $this->renderSideNav($panels)->getMenu(); } return parent::buildApplicationMenu(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $user = $this->getUser(); if (!$this->isSelf() && $user) { $username = $user->getUsername(); $crumbs->addTextCrumb($username, "/p/{$username}/"); } return $crumbs; } } diff --git a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php index 2759f3a26c..a3654a4388 100644 --- a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php @@ -1,46 +1,50 @@ getUser(); $user = $this->getUser(); $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $logs = id(new PhabricatorPeopleLogQuery()) ->setViewer($viewer) ->withRelatedPHIDs(array($user->getPHID())) ->executeWithCursorPager($pager); $table = id(new PhabricatorUserLogView()) ->setUser($viewer) ->setLogs($logs); $panel = $this->newBox(pht('Account Activity Logs'), $table); $pager_box = id(new PHUIBoxView()) ->addMargin(PHUI::MARGIN_LARGE) ->appendChild($pager); return array($panel, $pager_box); } public function isManagementPanel() { return true; } } diff --git a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php index 6ed6325d67..3ce72af2f8 100644 --- a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php @@ -1,20 +1,24 @@ getUser(); $viewer = $request->getUser(); $numbers = id(new PhabricatorAuthContactNumberQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->execute(); $rows = array(); foreach ($numbers as $number) { $rows[] = array( $number->newIconView(), phutil_tag( 'a', array( 'href' => $number->getURI(), ), $number->getDisplayName()), phabricator_datetime($number->getDateCreated(), $viewer), ); } $table = id(new AphrontTableView($rows)) ->setNoDataString( pht("You haven't added any contact numbers to your account.")) ->setHeaders( array( null, pht('Number'), pht('Created'), )) ->setColumnClasses( array( null, 'wide pri', 'right', )); $buttons = array(); $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') ->setText(pht('Add Contact Number')) ->setHref('/auth/contact/edit/') ->setColor(PHUIButtonView::GREY); return $this->newBox(pht('Contact Numbers'), $table, $buttons); } } diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php index e5ca46510e..285bc6989f 100644 --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -1,24 +1,28 @@ getUser()->getIsMailingList()) { return true; } return false; } public function processRequest(AphrontRequest $request) { $user = $this->getUser(); $editable = PhabricatorEnv::getEnvConfig('account.editable'); $uri = $request->getRequestURI(); $uri->setQueryParams(array()); if ($editable) { $new = $request->getStr('new'); if ($new) { return $this->returnNewAddressResponse($request, $uri, $new); } $delete = $request->getInt('delete'); if ($delete) { return $this->returnDeleteAddressResponse($request, $uri, $delete); } } $verify = $request->getInt('verify'); if ($verify) { return $this->returnVerifyAddressResponse($request, $uri, $verify); } $primary = $request->getInt('primary'); if ($primary) { return $this->returnPrimaryAddressResponse($request, $uri, $primary); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $user->getPHID()); $rowc = array(); $rows = array(); foreach ($emails as $email) { $button_verify = javelin_tag( 'a', array( 'class' => 'button small button-grey', 'href' => $uri->alter('verify', $email->getID()), 'sigil' => 'workflow', ), pht('Verify')); $button_make_primary = javelin_tag( 'a', array( 'class' => 'button small button-grey', 'href' => $uri->alter('primary', $email->getID()), 'sigil' => 'workflow', ), pht('Make Primary')); $button_remove = javelin_tag( 'a', array( 'class' => 'button small button-grey', 'href' => $uri->alter('delete', $email->getID()), 'sigil' => 'workflow', ), pht('Remove')); $button_primary = phutil_tag( 'a', array( 'class' => 'button small disabled', ), pht('Primary')); if (!$email->getIsVerified()) { $action = $button_verify; } else if ($email->getIsPrimary()) { $action = $button_primary; } else { $action = $button_make_primary; } if ($email->getIsPrimary()) { $remove = $button_primary; $rowc[] = 'highlighted'; } else { $remove = $button_remove; $rowc[] = null; } $rows[] = array( $email->getAddress(), $action, $remove, ); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Email'), pht('Status'), pht('Remove'), )); $table->setColumnClasses( array( 'wide', 'action', 'action', )); $table->setRowClasses($rowc); $table->setColumnVisibility( array( true, true, $editable, )); $buttons = array(); if ($editable) { $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') ->setText(pht('Add New Address')) ->setHref($uri->alter('new', 'true')) ->addSigil('workflow') ->setColor(PHUIButtonView::GREY); } return $this->newBox(pht('Email Addresses'), $table, $buttons); } private function returnNewAddressResponse( AphrontRequest $request, PhutilURI $uri, $new) { $user = $this->getUser(); $viewer = $this->getViewer(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); $e_email = true; $email = null; $errors = array(); if ($request->isDialogFormPost()) { $email = trim($request->getStr('email')); if ($new == 'verify') { // The user clicked "Done" from the "an email has been sent" dialog. return id(new AphrontReloadResponse())->setURI($uri); } PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorSettingsAddEmailAction(), 1); if (!strlen($email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isValidAddress($email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeValidAddresses(); } else if (!PhabricatorUserEmail::isAllowedAddress($email)) { $e_email = pht('Disallowed'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } if ($e_email === true) { $application_email = id(new PhabricatorMetaMTAApplicationEmailQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAddresses(array($email)) ->executeOne(); if ($application_email) { $e_email = pht('In Use'); $errors[] = $application_email->getInUseMessage(); } } if (!$errors) { $object = id(new PhabricatorUserEmail()) ->setAddress($email) ->setIsVerified(0); // If an administrator is editing a mailing list, automatically verify // the address. if ($viewer->getPHID() != $user->getPHID()) { if ($viewer->getIsAdmin()) { $object->setIsVerified(1); } } try { id(new PhabricatorUserEditor()) ->setActor($viewer) ->addEmail($user, $object); if ($object->getIsVerified()) { // If we autoverified the address, just reload the page. return id(new AphrontReloadResponse())->setURI($uri); } $object->sendVerificationEmail($user); $dialog = $this->newDialog() ->addHiddenInput('new', 'verify') ->setTitle(pht('Verification Email Sent')) ->appendChild(phutil_tag('p', array(), pht( 'A verification email has been sent. Click the link in the '. 'email to verify your address.'))) ->setSubmitURI($uri) ->addSubmitButton(pht('Done')); return id(new AphrontDialogResponse())->setDialog($dialog); } catch (AphrontDuplicateKeyQueryException $ex) { $e_email = pht('Duplicate'); $errors[] = pht('Another user already has this email.'); } } } if ($errors) { $errors = id(new PHUIInfoView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); $dialog = $this->newDialog() ->addHiddenInput('new', 'true') ->setTitle(pht('New Address')) ->appendChild($errors) ->appendChild($form) ->addSubmitButton(pht('Save')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnDeleteAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $this->getUser(); $viewer = $this->getViewer(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); // NOTE: You can only delete your own email addresses, and you can not // delete your primary address. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isPrimary = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { id(new PhabricatorUserEditor()) ->setActor($viewer) ->removeEmail($user, $email); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('delete', $email_id) ->setTitle(pht("Really delete address '%s'?", $address)) ->appendParagraph( pht( 'Are you sure you want to delete this address? You will no '. 'longer be able to use it to login.')) ->appendParagraph( pht( 'Note: Removing an email address from your account will invalidate '. 'any outstanding password reset links.')) ->addSubmitButton(pht('Delete')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnVerifyAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $this->getUser(); $viewer = $this->getViewer(); // NOTE: You can only send more email for your unverified addresses. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isVerified = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { $email->sendVerificationEmail($user); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('verify', $email_id) ->setTitle(pht('Send Another Verification Email?')) ->appendChild(phutil_tag('p', array(), pht( 'Send another copy of the verification email to %s?', $address))) ->addSubmitButton(pht('Send Email')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnPrimaryAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $this->getUser(); $viewer = $this->getViewer(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); // NOTE: You can only make your own verified addresses primary. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { id(new PhabricatorUserEditor()) ->setActor($viewer) ->changePrimaryEmail($user, $email); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('primary', $email_id) ->setTitle(pht('Change primary email address?')) ->appendParagraph( pht( 'If you change your primary address, Phabricator will send all '. 'email to %s.', $address)) ->appendParagraph( pht( 'Note: Changing your primary email address will invalidate any '. 'outstanding password reset links.')) ->addSubmitButton(pht('Change Primary Address')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php index 86260c1b5a..55932aa49b 100644 --- a/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php @@ -1,28 +1,32 @@ getUser()->getIsMailingList()) { return true; } return false; } public function isTemplatePanel() { return true; } } diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php index 51ff40ed9d..5a4a707a05 100644 --- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php @@ -1,39 +1,32 @@ isUserPanel()) { - return false; - } - - if ($this->getUser()->getIsMailingList()) { - return true; - } - - return false; -*/ } public function isTemplatePanel() { return true; } } diff --git a/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php index faa79889ed..defee73393 100644 --- a/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php @@ -1,213 +1,217 @@ getUser()->getIsMailingList()) { return true; } return false; } public function isTemplatePanel() { return true; } public function processRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $user = $this->getUser(); $preferences = $this->getPreferences(); $value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL; $errors = array(); if ($request->isFormPost()) { $new_tags = $request->getArr('mailtags'); $mailtags = $preferences->getPreference('mailtags', array()); $all_tags = $this->getAllTags($user); foreach ($all_tags as $key => $label) { $mailtags[$key] = (int)idx($new_tags, $key, $value_email); } $this->writeSetting( $preferences, PhabricatorEmailTagsSetting::SETTINGKEY, $mailtags); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $mailtags = $preferences->getSettingValue( PhabricatorEmailTagsSetting::SETTINGKEY); $form = id(new AphrontFormView()) ->setUser($viewer); $form->appendRemarkupInstructions( pht( 'You can adjust **Application Settings** here to customize when '. 'you are emailed and notified.'. "\n\n". "| Setting | Effect\n". "| ------- | -------\n". "| Email | You will receive an email and a notification, but the ". "notification will be marked \"read\".\n". "| Notify | You will receive an unread notification only.\n". "| Ignore | You will receive nothing.\n". "\n\n". 'If an update makes several changes (like adding CCs to a task, '. 'closing it, and adding a comment) you will receive the strongest '. 'notification any of the changes is configured to deliver.'. "\n\n". 'These preferences **only** apply to objects you are connected to '. '(for example, Revisions where you are a reviewer or tasks you are '. 'CC\'d on). To receive email alerts when other objects are created, '. 'configure [[ /herald/ | Herald Rules ]].')); $editors = $this->getAllEditorsWithTags($user); // Find all the tags shared by more than one application, and put them // in a "common" group. $all_tags = array(); foreach ($editors as $editor) { foreach ($editor->getMailTagsMap() as $tag => $name) { if (empty($all_tags[$tag])) { $all_tags[$tag] = array( 'count' => 0, 'name' => $name, ); } $all_tags[$tag]['count']; } } $common_tags = array(); foreach ($all_tags as $tag => $info) { if ($info['count'] > 1) { $common_tags[$tag] = $info['name']; } } // Build up the groups of application-specific options. $tag_groups = array(); foreach ($editors as $editor) { $tag_groups[] = array( $editor->getEditorObjectsDescription(), array_diff_key($editor->getMailTagsMap(), $common_tags), ); } // Sort them, then put "Common" at the top. $tag_groups = isort($tag_groups, 0); if ($common_tags) { array_unshift($tag_groups, array(pht('Common'), $common_tags)); } // Finally, build the controls. foreach ($tag_groups as $spec) { list($label, $map) = $spec; $control = $this->buildMailTagControl($label, $map, $mailtags); $form->appendChild($control); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Email Preferences')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form); return $form_box; } private function getAllEditorsWithTags(PhabricatorUser $user = null) { $editors = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorApplicationTransactionEditor') ->setFilterMethod('getMailTagsMap') ->execute(); foreach ($editors as $key => $editor) { // Remove editors for applications which are not installed. $app = $editor->getEditorApplicationClass(); if ($app !== null && $user !== null) { if (!PhabricatorApplication::isClassInstalledForViewer($app, $user)) { unset($editors[$key]); } } } return $editors; } private function getAllTags(PhabricatorUser $user = null) { $tags = array(); foreach ($this->getAllEditorsWithTags($user) as $editor) { $tags += $editor->getMailTagsMap(); } return $tags; } private function buildMailTagControl( $control_label, array $tags, array $prefs) { $value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL; $value_notify = PhabricatorEmailTagsSetting::VALUE_NOTIFY; $value_ignore = PhabricatorEmailTagsSetting::VALUE_IGNORE; $content = array(); foreach ($tags as $key => $label) { $select = AphrontFormSelectControl::renderSelectTag( (int)idx($prefs, $key, $value_email), array( $value_email => pht("\xE2\x9A\xAB Email"), $value_notify => pht("\xE2\x97\x90 Notify"), $value_ignore => pht("\xE2\x9A\xAA Ignore"), ), array( 'name' => 'mailtags['.$key.']', )); $content[] = phutil_tag( 'div', array( 'class' => 'psb', ), array( $select, ' ', $label, )); } $control = new AphrontFormStaticControl(); $control->setLabel($control_label); $control->setValue($content); return $control; } } diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php index e380248a83..1215487208 100644 --- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php @@ -1,137 +1,141 @@ getUser(); $providers = PhabricatorAuthProvider::getAllProviders(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $linked_head = pht('Linked Accounts and Authentication'); $linked = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString(pht('You have no linked accounts.')); $login_accounts = 0; foreach ($accounts as $account) { if ($account->isUsableForLogin()) { $login_accounts++; } } foreach ($accounts as $account) { $item = new PHUIObjectItemView(); $provider = idx($providers, $account->getProviderKey()); if ($provider) { $item->setHeader($provider->getProviderName()); $can_unlink = $provider->shouldAllowAccountUnlink(); if (!$can_unlink) { $item->addAttribute(pht('Permanently Linked')); } } else { $item->setHeader( pht('Unknown Account ("%s")', $account->getProviderKey())); $can_unlink = true; } $can_login = $account->isUsableForLogin(); if (!$can_login) { $item->addAttribute( pht( 'Disabled (an administrator has disabled login for this '. 'account provider).')); } $can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1)); $can_refresh = $provider && $provider->shouldAllowAccountRefresh(); if ($can_refresh) { $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-refresh') ->setHref('/auth/refresh/'.$account->getProviderKey().'/')); } $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->setWorkflow(true) ->setDisabled(!$can_unlink) ->setHref('/auth/unlink/'.$account->getProviderKey().'/')); if ($provider) { $provider->willRenderLinkedAccount($viewer, $item, $account); } $linked->addItem($item); } $linkable_head = pht('Add External Account'); $linkable = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht('Your account is linked with all available providers.')); $accounts = mpull($accounts, null, 'getProviderKey'); $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $providers = msort($providers, 'getProviderName'); foreach ($providers as $key => $provider) { if (isset($accounts[$key])) { continue; } if (!$provider->shouldAllowAccountLink()) { continue; } $link_uri = '/auth/link/'.$provider->getProviderKey().'/'; $item = id(new PHUIObjectItemView()) ->setHeader($provider->getProviderName()) ->setHref($link_uri) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-link') ->setHref($link_uri)); $linkable->addItem($item); } $linked_box = $this->newBox($linked_head, $linked); $linkable_box = $this->newBox($linkable_head, $linkable); return array( $linked_box, $linkable_box, ); } } diff --git a/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php index 9b846bd4b6..65a0be4e79 100644 --- a/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php @@ -1,24 +1,28 @@ getExists('new') || $request->getExists('providerPHID')) { return $this->processNew($request); } if ($request->getExists('edit')) { return $this->processEdit($request); } if ($request->getExists('delete')) { return $this->processDelete($request); } $user = $this->getUser(); $viewer = $request->getUser(); $factors = id(new PhabricatorAuthFactorConfigQuery()) ->setViewer($viewer) ->withUserPHIDs(array($user->getPHID())) ->setOrderVector(array('-id')) ->execute(); $rows = array(); $rowc = array(); $highlight_id = $request->getInt('id'); foreach ($factors as $factor) { $provider = $factor->getFactorProvider(); if ($factor->getID() == $highlight_id) { $rowc[] = 'highlighted'; } else { $rowc[] = null; } $rows[] = array( javelin_tag( 'a', array( 'href' => $this->getPanelURI('?edit='.$factor->getID()), 'sigil' => 'workflow', ), $factor->getFactorName()), $provider->getDisplayName(), phabricator_datetime($factor->getDateCreated(), $viewer), javelin_tag( 'a', array( 'href' => $this->getPanelURI('?delete='.$factor->getID()), 'sigil' => 'workflow', 'class' => 'small button button-grey', ), pht('Remove')), ); } $table = new AphrontTableView($rows); $table->setNoDataString( pht("You haven't added any authentication factors to your account yet.")); $table->setHeaders( array( pht('Name'), pht('Type'), pht('Created'), '', )); $table->setColumnClasses( array( 'wide pri', '', 'right', 'action', )); $table->setRowClasses($rowc); $table->setDeviceVisibility( array( true, false, false, true, )); $help_uri = PhabricatorEnv::getDoclink( 'User Guide: Multi-Factor Authentication'); $buttons = array(); $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') ->setText(pht('Add Auth Factor')) ->setHref($this->getPanelURI('?new=true')) ->setWorkflow(true) ->setColor(PHUIButtonView::GREY); $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-book') ->setText(pht('Help')) ->setHref($help_uri) ->setColor(PHUIButtonView::GREY); return $this->newBox(pht('Authentication Factors'), $table, $buttons); } private function processNew(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $cancel_uri = $this->getPanelURI(); // Check that we have providers before we send the user through the MFA // gate, so you don't authenticate and then immediately get roadblocked. $providers = id(new PhabricatorAuthFactorProviderQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorAuthFactorProvider::STATUS_ACTIVE)) ->execute(); if (!$providers) { return $this->newDialog() ->setTitle(pht('No MFA Providers')) ->appendParagraph( pht( 'There are no active MFA providers. At least one active provider '. 'must be available to add new MFA factors.')) ->addCancelButton($cancel_uri); } $providers = mpull($providers, null, 'getPHID'); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $cancel_uri); $selected_phid = $request->getStr('providerPHID'); if (empty($providers[$selected_phid])) { $selected_provider = null; } else { $selected_provider = $providers[$selected_phid]; } if (!$selected_provider) { $menu = id(new PHUIObjectItemListView()) ->setViewer($viewer) ->setBig(true) ->setFlush(true); foreach ($providers as $provider_phid => $provider) { $provider_uri = id(new PhutilURI($this->getPanelURI())) ->setQueryParam('providerPHID', $provider_phid); $item = id(new PHUIObjectItemView()) ->setHeader($provider->getDisplayName()) ->setHref($provider_uri) ->setClickable(true) ->setImageIcon($provider->newIconView()) ->addAttribute($provider->getDisplayDescription()); $menu->addItem($item); } return $this->newDialog() ->setTitle(pht('Choose Factor Type')) ->appendChild($menu) ->addCancelButton($cancel_uri); } $form = id(new AphrontFormView()) ->setViewer($viewer); $config = $selected_provider->processAddFactorForm( $form, $request, $user); if ($config) { $config->save(); $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), PhabricatorUserLog::ACTION_MULTI_ADD); $log->save(); $user->updateMultiFactorEnrollment(); // Terminate other sessions so they must log in and survive the // multi-factor auth check. id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $user, new PhutilOpaqueEnvelope( $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?id='.$config->getID())); } return $this->newDialog() ->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Add Authentication Factor')) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Continue')) ->addCancelButton($cancel_uri); } private function processEdit(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere( 'id = %d AND userPHID = %s', $request->getInt('edit'), $user->getPHID()); if (!$factor) { return new Aphront404Response(); } $e_name = true; $errors = array(); if ($request->isFormPost()) { $name = $request->getStr('name'); if (!strlen($name)) { $e_name = pht('Required'); $errors[] = pht( 'Authentication factors must have a name to identify them.'); } if (!$errors) { $factor->setFactorName($name); $factor->save(); $user->updateMultiFactorEnrollment(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?id='.$factor->getID())); } } else { $name = $factor->getFactorName(); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($name) ->setError($e_name)); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('edit', $factor->getID()) ->setTitle(pht('Edit Authentication Factor')) ->setErrors($errors) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Save')) ->addCancelButton($this->getPanelURI()); return id(new AphrontDialogResponse()) ->setDialog($dialog); } private function processDelete(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere( 'id = %d AND userPHID = %s', $request->getInt('delete'), $user->getPHID()); if (!$factor) { return new Aphront404Response(); } if ($request->isFormPost()) { $factor->delete(); $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), PhabricatorUserLog::ACTION_MULTI_REMOVE); $log->save(); $user->updateMultiFactorEnrollment(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI()); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('delete', $factor->getID()) ->setTitle(pht('Delete Authentication Factor')) ->appendParagraph( pht( 'Really remove the authentication factor %s from your account?', phutil_tag('strong', array(), $factor->getFactorName()))) ->addSubmitButton(pht('Remove Factor')) ->addCancelButton($this->getPanelURI()); return id(new AphrontDialogResponse()) ->setDialog($dialog); } } diff --git a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php index 797bcafcb3..d0165dc3f1 100644 --- a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php @@ -1,179 +1,183 @@ getViewer(); $preferences = $this->getPreferences(); $notifications_key = PhabricatorNotificationsSetting::SETTINGKEY; $notifications_value = $preferences->getSettingValue($notifications_key); if ($request->isFormPost()) { $this->writeSetting( $preferences, $notifications_key, $request->getInt($notifications_key)); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $title = pht('Notifications'); $control_id = celerity_generate_unique_node_id(); $status_id = celerity_generate_unique_node_id(); $browser_status_id = celerity_generate_unique_node_id(); $cancel_ask = pht( 'The dialog asking for permission to send desktop notifications was '. 'closed without granting permission. Only application notifications '. 'will be sent.'); $accept_ask = pht( 'Click "Save Preference" to persist these changes.'); $reject_ask = pht( 'Permission for desktop notifications was denied. Only application '. 'notifications will be sent.'); $no_support = pht( 'This web browser does not support desktop notifications. Only '. 'application notifications will be sent for this browser regardless of '. 'this preference.'); $default_status = phutil_tag( 'span', array(), array( pht('This browser has not yet granted permission to send desktop '. 'notifications for this Phabricator instance.'), phutil_tag('br'), phutil_tag('br'), javelin_tag( 'button', array( 'sigil' => 'desktop-notifications-permission-button', 'class' => 'green', ), pht('Grant Permission')), )); $granted_status = phutil_tag( 'span', array(), pht('This browser has been granted permission to send desktop '. 'notifications for this Phabricator instance.')); $denied_status = phutil_tag( 'span', array(), pht('This browser has denied permission to send desktop notifications '. 'for this Phabricator instance. Consult your browser settings / '. 'documentation to figure out how to clear this setting, do so, '. 'and then re-visit this page to grant permission.')); $message_id = celerity_generate_unique_node_id(); $message_container = phutil_tag( 'span', array( 'id' => $message_id, )); $saved_box = null; if ($request->getBool('saved')) { $saved_box = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('Changes saved.')); } $status_box = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setID($status_id) ->setIsHidden(true) ->appendChild($message_container); $status_box = id(new PHUIBoxView()) ->addClass('mll mlr') ->appendChild($status_box); $control_config = array( 'controlID' => $control_id, 'statusID' => $status_id, 'messageID' => $message_id, 'browserStatusID' => $browser_status_id, 'defaultMode' => 0, 'desktop' => 1, 'desktopOnly' => 2, 'cancelAsk' => $cancel_ask, 'grantedAsk' => $accept_ask, 'deniedAsk' => $reject_ask, 'defaultStatus' => $default_status, 'deniedStatus' => $denied_status, 'grantedStatus' => $granted_status, 'noSupport' => $no_support, ); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel($title) ->setControlID($control_id) ->setName($notifications_key) ->setValue($notifications_value) ->setOptions(PhabricatorNotificationsSetting::getOptionsMap()) ->setCaption( pht( 'Phabricator can send real-time notifications to your web browser '. 'or to your desktop. Select where you want to receive these '. 'real-time updates.')) ->initBehavior( 'desktop-notifications-control', $control_config)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preference'))); $button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-send-o') ->setWorkflow(true) ->setText(pht('Send Test Notification')) ->setHref('/notification/test/') ->setColor(PHUIButtonView::GREY); $form_content = array($saved_box, $status_box, $form); $form_box = $this->newBox( pht('Notifications'), $form_content, array($button)); $browser_status_box = id(new PHUIInfoView()) ->setID($browser_status_id) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setIsHidden(true) ->appendChild($default_status); return array( $form_box, $browser_status_box, ); } } diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php index 79d7610f2f..37393d5d4f 100644 --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -1,218 +1,222 @@ getUser(); $user = $this->getUser(); $content_source = PhabricatorContentSource::newFromRequest($request); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, '/settings/'); $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); $min_len = (int)$min_len; // 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. $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; $password_objects = id(new PhabricatorAuthPasswordQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->withPasswordTypes(array($account_type)) ->withIsRevoked(false) ->execute(); if ($password_objects) { $password_object = head($password_objects); } else { $password_object = PhabricatorAuthPassword::initializeNewPassword( $user, $account_type); } $e_old = true; $e_new = true; $e_conf = true; $errors = array(); if ($request->isFormPost()) { // Rate limit guesses about the old password. This page requires MFA and // session compromise already, so this is mostly just to stop researchers // from reporting this as a vulnerability. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthChangePasswordAction(), 1); $envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw')); $engine = id(new PhabricatorAuthPasswordEngine()) ->setViewer($viewer) ->setContentSource($content_source) ->setPasswordType($account_type) ->setObject($user); if (!strlen($envelope->openEnvelope())) { $errors[] = pht('You must enter your current password.'); $e_old = pht('Required'); } else if (!$engine->isValidPassword($envelope)) { $errors[] = pht('The old password you entered is incorrect.'); $e_old = pht('Invalid'); } else { $e_old = null; // Refund the user an action credit for getting the password right. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthChangePasswordAction(), -1); } $pass = $request->getStr('new_pw'); $conf = $request->getStr('conf_pw'); $password_envelope = new PhutilOpaqueEnvelope($pass); $confirm_envelope = new PhutilOpaqueEnvelope($conf); try { $engine->checkNewPassword($password_envelope, $confirm_envelope); $e_new = null; $e_conf = null; } catch (PhabricatorAuthPasswordException $ex) { $errors[] = $ex->getMessage(); $e_new = $ex->getPasswordError(); $e_conf = $ex->getConfirmError(); } if (!$errors) { $password_object ->setPassword($password_envelope, $user) ->save(); $next = $this->getPanelURI('?saved=true'); id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $user, new PhutilOpaqueEnvelope( $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); return id(new AphrontRedirectResponse())->setURI($next); } } if ($password_object->getID()) { try { $can_upgrade = $password_object->canUpgrade(); } catch (PhabricatorPasswordHasherUnavailableException $ex) { $can_upgrade = false; $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) { $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 = id(new AphrontFormView()) ->setViewer($viewer) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Old Password')) ->setError($e_old) ->setName('old_pw')) ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setLabel(pht('New Password')) ->setError($e_new) ->setName('new_pw')) ->appendChild( id(new AphrontFormPasswordControl()) ->setDisableAutocomplete(true) ->setLabel(pht('Confirm Password')) ->setCaption($len_caption) ->setError($e_conf) ->setName('conf_pw')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Change Password'))); $properties = id(new PHUIPropertyListView()); $properties->addProperty( pht('Current Algorithm'), PhabricatorPasswordHasher::getCurrentAlgorithmName( $password_object->newPasswordEnvelope())); $properties->addProperty( pht('Best Available Algorithm'), PhabricatorPasswordHasher::getBestAlgorithmName()); $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild( pht('Changing your password will terminate any other outstanding '. 'login sessions.')); $algo_box = $this->newBox(pht('Password Algorithms'), $properties); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Change Password')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form); return array( $form_box, $algo_box, $info_view, ); } } diff --git a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php index 13944411ed..131f602974 100644 --- a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php @@ -1,51 +1,55 @@ getUser()->getIsMailingList()) { return false; } return true; } public function getPanelKey() { return 'ssh'; } public function getPanelName() { return pht('SSH Public Keys'); } + public function getPanelMenuIcon() { + return 'fa-file-text-o'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } public function processRequest(AphrontRequest $request) { $user = $this->getUser(); $viewer = $request->getUser(); $keys = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($user->getPHID())) ->withIsActive(true) ->execute(); $table = id(new PhabricatorAuthSSHKeyTableView()) ->setUser($viewer) ->setKeys($keys) ->setCanEdit(true) ->setNoDataString(pht("You haven't added any SSH Public Keys.")); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $ssh_actions = PhabricatorAuthSSHKeyTableView::newKeyActionsMenu( $viewer, $user); return $this->newBox(pht('SSH Public Keys'), $table, array($ssh_actions)); } } diff --git a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php index 314d68f69d..fb10572e11 100644 --- a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php @@ -1,139 +1,143 @@ getUser(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $identity_phids = mpull($accounts, 'getPHID'); $identity_phids[] = $viewer->getPHID(); $sessions = id(new PhabricatorAuthSessionQuery()) ->setViewer($viewer) ->withIdentityPHIDs($identity_phids) ->execute(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($identity_phids) ->execute(); $current_key = PhabricatorAuthSession::newSessionDigest( new PhutilOpaqueEnvelope( $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); $rows = array(); $rowc = array(); foreach ($sessions as $session) { $is_current = phutil_hashes_are_identical( $session->getSessionKey(), $current_key); if ($is_current) { $rowc[] = 'highlighted'; $button = phutil_tag( 'a', array( 'class' => 'small button button-grey disabled', ), pht('Current')); } else { $rowc[] = null; $button = javelin_tag( 'a', array( 'href' => '/auth/session/terminate/'.$session->getID().'/', 'class' => 'small button button-grey', 'sigil' => 'workflow', ), pht('Terminate')); } $hisec = ($session->getHighSecurityUntil() - time()); $rows[] = array( $handles[$session->getUserPHID()]->renderLink(), substr($session->getSessionKey(), 0, 6), $session->getType(), ($hisec > 0) ? phutil_format_relative_time($hisec) : null, phabricator_datetime($session->getSessionStart(), $viewer), phabricator_date($session->getSessionExpires(), $viewer), $button, ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht("You don't have any active sessions.")); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Identity'), pht('Session'), pht('Type'), pht('HiSec'), pht('Created'), pht('Expires'), pht(''), )); $table->setColumnClasses( array( 'wide', 'n', '', 'right', 'right', 'right', 'action', )); $buttons = array(); $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-warning') ->setText(pht('Terminate All Sessions')) ->setHref('/auth/session/terminate/all/') ->setWorkflow(true) ->setColor(PHUIButtonView::RED); $hisec = ($viewer->getSession()->getHighSecurityUntil() - time()); if ($hisec > 0) { $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-lock') ->setText(pht('Leave High Security')) ->setHref('/auth/session/downgrade/') ->setWorkflow(true) ->setColor(PHUIButtonView::RED); } return $this->newBox(pht('Active Login Sessions'), $table, $buttons); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php index 19ac6fec62..8250418812 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php @@ -1,301 +1,311 @@ user = $user; return $this; } public function getUser() { return $this->user; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setOverrideURI($override_uri) { $this->overrideURI = $override_uri; return $this; } final public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } final public function getController() { return $this->controller; } final public function setNavigation(AphrontSideNavFilterView $navigation) { $this->navigation = $navigation; return $this; } final public function getNavigation() { return $this->navigation; } public function setPreferences(PhabricatorUserPreferences $preferences) { $this->preferences = $preferences; return $this; } public function getPreferences() { return $this->preferences; } final public static function getAllPanels() { $panels = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getPanelKey') ->execute(); return msortv($panels, 'getPanelOrderVector'); } final public static function getAllDisplayPanels() { $panels = array(); $groups = PhabricatorSettingsPanelGroup::getAllPanelGroupsWithPanels(); foreach ($groups as $group) { foreach ($group->getPanels() as $key => $panel) { $panels[$key] = $panel; } } return $panels; } final public function getPanelGroup() { $group_key = $this->getPanelGroupKey(); $groups = PhabricatorSettingsPanelGroup::getAllPanelGroupsWithPanels(); $group = idx($groups, $group_key); if (!$group) { throw new Exception( pht( 'No settings panel group with key "%s" exists!', $group_key)); } return $group; } /* -( Panel Configuration )------------------------------------------------ */ /** * Return a unique string used in the URI to identify this panel, like * "example". * * @return string Unique panel identifier (used in URIs). * @task config */ public function getPanelKey() { return $this->getPhobjectClassConstant('PANELKEY'); } /** * Return a human-readable description of the panel's contents, like * "Example Settings". * * @return string Human-readable panel name. * @task config */ abstract public function getPanelName(); + /** + * Return an icon for the panel in the menu. + * + * @return string Icon identifier. + * @task config + */ + public function getPanelMenuIcon() { + return 'fa-wrench'; + } + /** * Return a panel group key constant for this panel. * * @return const Panel group key. * @task config */ abstract public function getPanelGroupKey(); /** * Return false to prevent this panel from being displayed or used. You can * do, e.g., configuration checks here, to determine if the feature your * panel controls is unavailable in this install. By default, all panels are * enabled. * * @return bool True if the panel should be shown. * @task config */ public function isEnabled() { return true; } /** * Return true if this panel is available to users while editing their own * settings. * * @return bool True to enable management on behalf of a user. * @task config */ public function isUserPanel() { return true; } /** * Return true if this panel is available to administrators while managing * bot and mailing list accounts. * * @return bool True to enable management on behalf of accounts. * @task config */ public function isManagementPanel() { return false; } /** * Return true if this panel is available while editing settings templates. * * @return bool True to allow editing in templates. * @task config */ public function isTemplatePanel() { return false; } /* -( Panel Implementation )----------------------------------------------- */ /** * Process a user request for this settings panel. Implement this method like * a lightweight controller. If you return an @{class:AphrontResponse}, the * response will be used in whole. If you return anything else, it will be * treated as a view and composed into a normal settings page. * * Generally, render your settings panel by returning a form, then return * a redirect when the user saves settings. * * @param AphrontRequest Incoming request. * @return wild Response to request, either as an * @{class:AphrontResponse} or something which can * be composed into a @{class:AphrontView}. * @task panel */ abstract public function processRequest(AphrontRequest $request); /** * Get the URI for this panel. * * @param string? Optional path to append. * @return string Relative URI for the panel. * @task panel */ final public function getPanelURI($path = '') { $path = ltrim($path, '/'); if ($this->overrideURI) { return rtrim($this->overrideURI, '/').'/'.$path; } $key = $this->getPanelKey(); $key = phutil_escape_uri($key); $user = $this->getUser(); if ($user) { if ($user->isLoggedIn()) { $username = $user->getUsername(); return "/settings/user/{$username}/page/{$key}/{$path}"; } else { // For logged-out users, we can't put their username in the URI. This // page will prompt them to login, then redirect them to the correct // location. return "/settings/panel/{$key}/"; } } else { $builtin = $this->getPreferences()->getBuiltinKey(); return "/settings/builtin/{$builtin}/page/{$key}/{$path}"; } } /* -( Internals )---------------------------------------------------------- */ /** * Generates a key to sort the list of panels. * * @return string Sortable key. * @task internal */ final public function getPanelOrderVector() { return id(new PhutilSortVector()) ->addString($this->getPanelName()); } protected function newDialog() { return $this->getController()->newDialog(); } protected function writeSetting( PhabricatorUserPreferences $preferences, $key, $value) { $viewer = $this->getViewer(); $request = $this->getController()->getRequest(); $editor = id(new PhabricatorUserPreferencesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = array(); $xactions[] = $preferences->newTransaction($key, $value); $editor->applyTransactions($preferences, $xactions); } public function newBox($title, $content, $actions = array()) { $header = id(new PHUIHeaderView()) ->setHeader($title); foreach ($actions as $action) { $header->addActionLink($action); } $view = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($content) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG); return $view; } } diff --git a/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php b/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php index f2021bafa5..91064a432f 100644 --- a/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php @@ -1,85 +1,89 @@ getUser(); $tokens = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer($viewer) ->withTokenResources(array($viewer->getPHID())) ->execute(); $rows = array(); foreach ($tokens as $token) { if ($token->isRevocable()) { $button = javelin_tag( 'a', array( 'href' => '/auth/token/revoke/'.$token->getID().'/', 'class' => 'small button button-grey', 'sigil' => 'workflow', ), pht('Revoke')); } else { $button = javelin_tag( 'a', array( 'class' => 'small button button-grey disabled', ), pht('Revoke')); } if ($token->getTokenExpires() >= time()) { $expiry = phabricator_datetime($token->getTokenExpires(), $viewer); } else { $expiry = pht('Expired'); } $rows[] = array( $token->getTokenReadableTypeName(), $expiry, $button, ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht("You don't have any active tokens.")); $table->setHeaders( array( pht('Type'), pht('Expires'), pht(''), )); $table->setColumnClasses( array( 'wide', 'right', 'action', )); $button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-warning') ->setText(pht('Revoke All')) ->setHref('/auth/token/revoke/all/') ->setWorkflow(true) ->setColor(PHUIButtonView::RED); return $this->newBox(pht('Temporary Tokens'), $table, array($button)); } }