From 5dafabd5b4d039bd06b7c773a70a79236ea7409e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Oct 2019 09:06:13 -0700 Subject: [PATCH 01/57] Fix deprecated argument order for "implode()" Summary: Fixes T13428. In modern PHP, "implode()" should take the glue parameter first. Test Plan: Used the linter introduced in D20857 to identify affected callsites. ``` $ git grep -i implode | cut -d: -f1 | sort | uniq | xargs arc lint --output summary --never-apply-patches | grep -i glue ``` Maniphest Tasks: T13428 Differential Revision: https://secure.phabricator.com/D20858 --- .../PhabricatorDifferentialRevisionTestDataGenerator.php | 2 +- src/applications/people/view/PhabricatorUserCardView.php | 2 +- src/applications/project/view/PhabricatorProjectCardView.php | 2 +- src/view/phui/PHUIPropertyListView.php | 4 ++-- src/view/phui/PHUITwoColumnView.php | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php index 26632dff24..84a4cbc519 100644 --- a/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php +++ b/src/applications/differential/lipsum/PhabricatorDifferentialRevisionTestDataGenerator.php @@ -103,7 +103,7 @@ final class PhabricatorDifferentialRevisionTestDataGenerator $newcode2[] = $altcodearr[$randomlines_new[$c++]]; } } - return implode($newcode2, "\n"); + return implode("\n", $newcode2); } } diff --git a/src/applications/people/view/PhabricatorUserCardView.php b/src/applications/people/view/PhabricatorUserCardView.php index 21cb468ba8..54d0a41204 100644 --- a/src/applications/people/view/PhabricatorUserCardView.php +++ b/src/applications/people/view/PhabricatorUserCardView.php @@ -38,7 +38,7 @@ final class PhabricatorUserCardView extends AphrontTagView { } return array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ); } diff --git a/src/applications/project/view/PhabricatorProjectCardView.php b/src/applications/project/view/PhabricatorProjectCardView.php index f82ee8c99e..a56697ba7e 100644 --- a/src/applications/project/view/PhabricatorProjectCardView.php +++ b/src/applications/project/view/PhabricatorProjectCardView.php @@ -36,7 +36,7 @@ final class PhabricatorProjectCardView extends AphrontTagView { $classes[] = 'project-card-'.$color; return array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ); } diff --git a/src/view/phui/PHUIPropertyListView.php b/src/view/phui/PHUIPropertyListView.php index 336c494a3c..62fa30bba8 100644 --- a/src/view/phui/PHUIPropertyListView.php +++ b/src/view/phui/PHUIPropertyListView.php @@ -284,7 +284,7 @@ final class PHUIPropertyListView extends AphrontView { return phutil_tag( 'div', array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ), $part['content']); } @@ -295,7 +295,7 @@ final class PHUIPropertyListView extends AphrontView { return phutil_tag( 'div', array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ), $part['content']); } diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index 9240887f4b..cf4abe3a40 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -220,7 +220,7 @@ final class PHUITwoColumnView extends AphrontTagView { return phutil_tag( 'div', array( - 'class' => implode($classes, ' '), + 'class' => implode(' ', $classes), ), array( $navigation, From d34dfa37461b3c7ef598df4309a83ac3fd8970fc Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Oct 2019 09:14:21 -0700 Subject: [PATCH 02/57] Fix an error message when calling "transaction.search" with a non-transactional object PHID as an "objectIdentifier" Summary: See PHI1499. This error message doesn't provide parameters, and can be a little bit more helpful. Test Plan: {F6957550} Differential Revision: https://secure.phabricator.com/D20859 --- .../conduit/TransactionSearchConduitAPIMethod.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 6f7f713dab..bc0311eae4 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -103,8 +103,11 @@ EOREMARKUP if (!($object instanceof PhabricatorApplicationTransactionInterface)) { throw new Exception( pht( - 'Object "%s" does not implement "%s", so transactions can not '. - 'be loaded for it.')); + 'Object "%s" (of type "%s") does not implement "%s", so '. + 'transactions can not be loaded for it.', + $object_name, + get_class($object), + 'PhabricatorApplicationTransactionInterface')); } $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject( From f497b93e4311b424b24599f23c272ecb912ab5a6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 17 Oct 2019 09:39:26 -0700 Subject: [PATCH 03/57] Fix a fatal in the "Projects" curtain extension when a project edge connects an object to a non-project Summary: Ref T13429. It's currently possible to write "TYPE_EDGE" relationships for the "object has project" edge to PHIDs which may not actually be projects. Today, this fatals. As a first step, unfatal it. T13429 discusses general improvements and greater context. Test Plan: Used "maniphest.edit" to write a "project" edge to a user PHID, viewed the task in the UI. Previously it fataled; now it renders unusually (the object is "tagged" with a user) but faithfully reflects database state. {F6957606} Maniphest Tasks: T13429 Differential Revision: https://secure.phabricator.com/D20860 --- src/__phutil_library_map__.php | 2 ++ .../project/engine/PhabricatorBoardLayoutEngine.php | 6 ++++++ .../project/interface/PhabricatorWorkboardInterface.php | 3 +++ src/applications/project/storage/PhabricatorProject.php | 3 ++- 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/applications/project/interface/PhabricatorWorkboardInterface.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 41befc5ce3..189ecdf0bf 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5044,6 +5044,7 @@ phutil_register_library_map(array( 'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php', 'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', + 'PhabricatorWorkboardInterface' => 'applications/project/interface/PhabricatorWorkboardInterface.php', 'PhabricatorWorkboardViewState' => 'applications/project/state/PhabricatorWorkboardViewState.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', @@ -10748,6 +10749,7 @@ phutil_register_library_map(array( 'PhabricatorColumnProxyInterface', 'PhabricatorSpacesInterface', 'PhabricatorEditEngineSubtypeInterface', + 'PhabricatorWorkboardInterface', ), 'PhabricatorProjectActivityChartEngine' => 'PhabricatorChartEngine', 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 22aaed2d4b..a0be7e5775 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -301,6 +301,12 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->execute(); $boards = mpull($boards, null, 'getPHID'); + foreach ($boards as $key => $board) { + if (!($board instanceof PhabricatorWorkboardInterface)) { + unset($boards[$key]); + } + } + if (!$this->fetchAllBoards) { foreach ($boards as $key => $board) { if (!$board->getHasWorkboard()) { diff --git a/src/applications/project/interface/PhabricatorWorkboardInterface.php b/src/applications/project/interface/PhabricatorWorkboardInterface.php new file mode 100644 index 0000000000..2b760b4875 --- /dev/null +++ b/src/applications/project/interface/PhabricatorWorkboardInterface.php @@ -0,0 +1,3 @@ + Date: Thu, 24 Oct 2019 16:42:25 -0700 Subject: [PATCH 04/57] Add default branch, description, and metrics (commit count, recent commit) to "diffusion.repository.search" Summary: Fixes T13430. Provide more information about repositories in "diffusion.repository.search". Test Plan: Used API console to call method (with new "metrics" attachment), reviewed output. Saw new fields returned. Maniphest Tasks: T13430 Differential Revision: https://secure.phabricator.com/D20862 --- src/__phutil_library_map__.php | 2 + ...epositoryMetricsSearchEngineAttachment.php | 41 +++++++++++++++++++ .../query/PhabricatorRepositoryQuery.php | 2 + .../storage/PhabricatorRepository.php | 19 +++++++++ 4 files changed, 64 insertions(+) create mode 100644 src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 189ecdf0bf..9d51d92760 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -996,6 +996,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryManagementOtherPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementOtherPanelGroup.php', 'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php', 'DiffusionRepositoryManagementPanelGroup' => 'applications/diffusion/management/DiffusionRepositoryManagementPanelGroup.php', + 'DiffusionRepositoryMetricsSearchEngineAttachment' => 'applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryPoliciesManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryPoliciesManagementPanel.php', 'DiffusionRepositoryProfilePictureController' => 'applications/diffusion/controller/DiffusionRepositoryProfilePictureController.php', @@ -6964,6 +6965,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryManagementOtherPanelGroup' => 'DiffusionRepositoryManagementPanelGroup', 'DiffusionRepositoryManagementPanel' => 'Phobject', 'DiffusionRepositoryManagementPanelGroup' => 'Phobject', + 'DiffusionRepositoryMetricsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'DiffusionRepositoryPath' => 'Phobject', 'DiffusionRepositoryPoliciesManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryProfilePictureController' => 'DiffusionController', diff --git a/src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php b/src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php new file mode 100644 index 0000000000..9711e72352 --- /dev/null +++ b/src/applications/diffusion/engineextension/DiffusionRepositoryMetricsSearchEngineAttachment.php @@ -0,0 +1,41 @@ +needCommitCounts(true) + ->needMostRecentCommits(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $commit = $object->getMostRecentCommit(); + if ($commit !== null) { + $recent_commit = $commit->getFieldValuesForConduit(); + } else { + $recent_commit = null; + } + + $commit_count = $object->getCommitCount(); + if ($commit_count !== null) { + $commit_count = (int)$commit_count; + } + + return array( + 'commitCount' => $commit_count, + 'recentCommit' => $recent_commit, + ); + } + +} diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index e960cf888b..21465393f7 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -215,6 +215,8 @@ final class PhabricatorRepositoryQuery $commits = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withIDs($commit_ids) + ->needCommitData(true) + ->needIdentities(true) ->execute(); } else { $commits = array(); diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index fdc9a695c4..d568c755c5 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2757,6 +2757,14 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO ->setDescription( pht( 'The "Fetch" and "Permanent Ref" rules for this repository.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('defaultBranch') + ->setType('string?') + ->setDescription(pht('Default branch name.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('description') + ->setType('remarkup') + ->setDescription(pht('Repository description.')), ); } @@ -2769,6 +2777,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $track_rules = $this->getStringListForConduit($track_rules); $permanent_rules = $this->getStringListForConduit($permanent_rules); + $default_branch = $this->getDefaultBranch(); + if (!strlen($default_branch)) { + $default_branch = null; + } + return array( 'name' => $this->getName(), 'vcs' => $this->getVersionControlSystem(), @@ -2782,6 +2795,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO 'trackRules' => $track_rules, 'permanentRefRules' => $permanent_rules, ), + 'defaultBranch' => $default_branch, + 'description' => array( + 'raw' => (string)$this->getDetail('description'), + ), ); } @@ -2804,6 +2821,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return array( id(new DiffusionRepositoryURIsSearchEngineAttachment()) ->setAttachmentKey('uris'), + id(new DiffusionRepositoryMetricsSearchEngineAttachment()) + ->setAttachmentKey('metrics'), ); } From 633aa5288c58d507d8227b1007e3f1dca7cb1e4f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 24 Oct 2019 18:03:11 -0700 Subject: [PATCH 05/57] Persist login instructions onto flow-specific login pages (username/password and LDAP) Summary: Fixes T13433. Currently, "Login Screen Instructions" in "Auth" are shown only on the main login screen. If you enter a bad password or bad LDAP credential set and move to the flow-specific login failure screen (for example, "invalid password"), the instructions vanish. Instead, persist them. There are reasonable cases where this is highly useful and the cases which spring to mind where this is possibly misleading are fairly easy to fix by making the instructions more specific. Test Plan: - Configured login instructions in "Auth". - Viewed main login screen, saw instructions. - Entered a bad username/password and a bad LDAP credential set, got kicked to workflow sub-pages and still saw instructions (previously: no instructions). - Grepped for other callers to `buildProviderPageResponse()` to look for anything weird, came up empty. Maniphest Tasks: T13433 Differential Revision: https://secure.phabricator.com/D20863 --- resources/celerity/map.php | 6 ++--- .../controller/PhabricatorAuthController.php | 22 +++++++++++++++++++ .../PhabricatorAuthLoginController.php | 12 +++++++--- .../PhabricatorAuthStartController.php | 21 ------------------ webroot/rsrc/css/application/auth/auth.css | 2 +- 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 682f75a16b..0e53737aaf 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '88366522', + 'core.pkg.css' => '686ae87c', 'core.pkg.js' => '6e5c894f', 'differential.pkg.css' => '607c84be', 'differential.pkg.js' => 'a0212a0b', @@ -36,7 +36,7 @@ return array( 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', 'rsrc/css/aphront/typeahead.css' => '8779483d', 'rsrc/css/application/almanac/almanac.css' => '2e050f4f', - 'rsrc/css/application/auth/auth.css' => 'add92fd8', + 'rsrc/css/application/auth/auth.css' => 'c2f23d74', 'rsrc/css/application/base/main-menu-view.css' => '17b71bbc', 'rsrc/css/application/base/notification-menu.css' => '4df1ee30', 'rsrc/css/application/base/phui-theme.css' => '35883b37', @@ -540,7 +540,7 @@ return array( 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', 'application-search-view-css' => '0f7c06d8', - 'auth-css' => 'add92fd8', + 'auth-css' => 'c2f23d74', 'bulk-job-css' => '73af99f5', 'conduit-api-css' => 'ce2cfc41', 'config-options-css' => '16c920ae', diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index cda56d34b1..505620b3f3 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -286,4 +286,26 @@ abstract class PhabricatorAuthController extends PhabricatorController { ->appendChild($invite_list); } + + final protected function newCustomStartMessage() { + $viewer = $this->getViewer(); + + $text = PhabricatorAuthMessage::loadMessageText( + $viewer, + PhabricatorAuthLoginMessageType::MESSAGEKEY); + + if (!strlen($text)) { + return null; + } + + $remarkup_view = new PHUIRemarkupView($viewer, $text); + + return phutil_tag( + 'div', + array( + 'class' => 'auth-custom-message', + ), + $remarkup_view); + } + } diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index e7dabd9340..b46cf36d49 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -238,18 +238,24 @@ final class PhabricatorAuthLoginController $content) { $crumbs = $this->buildApplicationCrumbs(); + $viewer = $this->getViewer(); - if ($this->getRequest()->getUser()->isLoggedIn()) { + if ($viewer->isLoggedIn()) { $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI()); } else { - $crumbs->addTextCrumb(pht('Log In'), $this->getApplicationURI('start/')); + $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/')); + + $content = array( + $this->newCustomStartMessage(), + $content, + ); } $crumbs->addTextCrumb($provider->getProviderName()); $crumbs->setBorder(true); return $this->newPage() - ->setTitle(pht('Log In')) + ->setTitle(pht('Login')) ->setCrumbs($crumbs) ->appendChild($content); } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 72cbbea5a8..7e9b17feff 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -298,27 +298,6 @@ final class PhabricatorAuthStartController ->setURI($auto_uri); } - private function newCustomStartMessage() { - $viewer = $this->getViewer(); - - $text = PhabricatorAuthMessage::loadMessageText( - $viewer, - PhabricatorAuthLoginMessageType::MESSAGEKEY); - - if (!strlen($text)) { - return null; - } - - $remarkup_view = new PHUIRemarkupView($viewer, $text); - - return phutil_tag( - 'div', - array( - 'class' => 'auth-custom-message', - ), - $remarkup_view); - } - private function newEmailLoginView(array $configs) { assert_instances_of($configs, 'PhabricatorAuthProviderConfig'); diff --git a/webroot/rsrc/css/application/auth/auth.css b/webroot/rsrc/css/application/auth/auth.css index 687aaf2bb4..28b18b85c5 100644 --- a/webroot/rsrc/css/application/auth/auth.css +++ b/webroot/rsrc/css/application/auth/auth.css @@ -57,7 +57,7 @@ } .auth-custom-message { - margin: 32px auto 64px; + margin: 32px auto 48px; max-width: 548px; background: #fff; padding: 16px; From 38694578e1d0c416e992a0fa95edbc270c8af5e8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 24 Oct 2019 18:30:55 -0700 Subject: [PATCH 06/57] Improve project member list behaviors related to disabled users Summary: Fixes T13431. Increase the "panel" version of project member lists to 10 users, hide disabled users, swap the buttons to "tail buttons". Sort disabled users to the bottom of "full list" versions of member lists. For UI consistency, render the remove "X" as disabled but visible if users don't have permission to remove members. Test Plan: - Viewed a project with disabled members. - Saw only enabled members on the main project page. - Saw disabled members sorted to the bottom on the members page. - Clicked "View All" to jump from the panel to the members page. - As a user who could not edit a project, viewed the members page and saw a disabled "X" with a policy error when clicked. - Removed a member as before, as a normal user with permission to remove members. Maniphest Tasks: T13431 Differential Revision: https://secure.phabricator.com/D20864 --- .../PhabricatorProjectProfileController.php | 4 +- .../view/PhabricatorProjectUserListView.php | 86 ++++++++++++------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 386a649238..54e1d77d55 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -63,14 +63,14 @@ final class PhabricatorProjectProfileController $member_list = id(new PhabricatorProjectMemberListView()) ->setUser($viewer) ->setProject($project) - ->setLimit(5) + ->setLimit(10) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getMemberPHIDs()); $watcher_list = id(new PhabricatorProjectWatcherListView()) ->setUser($viewer) ->setProject($project) - ->setLimit(5) + ->setLimit(10) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getWatcherPHIDs()); diff --git a/src/applications/project/view/PhabricatorProjectUserListView.php b/src/applications/project/view/PhabricatorProjectUserListView.php index 51c2ced6d1..dd3e084c6e 100644 --- a/src/applications/project/view/PhabricatorProjectUserListView.php +++ b/src/applications/project/view/PhabricatorProjectUserListView.php @@ -1,6 +1,7 @@ getUserPHIDs(); $can_edit = $this->canEditList(); + $supports_edit = $project->supportsEditMembers(); $no_data = $this->getNoDataString(); $list = id(new PHUIObjectItemListView()) ->setNoDataString($no_data); $limit = $this->getLimit(); - - // If we're showing everything, show oldest to newest. If we're showing - // only a slice, show newest to oldest. - if (!$limit) { - $user_phids = array_reverse($user_phids); - } + $is_panel = (bool)$limit; $handles = $viewer->loadHandles($user_phids); - // Always put the viewer first if they are on the list. - $user_phids = array_fuse($user_phids); - $user_phids = - array_select_keys($user_phids, array($viewer->getPHID())) + - $user_phids; + // Reorder users in display order. We're going to put the viewer first + // if they're a member, then enabled users, then disabled/invalid users. - if ($limit) { - $render_phids = array_slice($user_phids, 0, $limit); - } else { - $render_phids = $user_phids; - } - - foreach ($render_phids as $user_phid) { + $phid_map = array(); + foreach ($user_phids as $user_phid) { $handle = $handles[$user_phid]; + $is_viewer = ($user_phid === $viewer->getPHID()); + $is_enabled = ($handle->isComplete() && !$handle->isDisabled()); + + // If we're showing the main member list, show oldest to newest. If we're + // showing only a slice in a panel, show newest to oldest. + if ($limit) { + $order_scalar = 1; + } else { + $order_scalar = -1; + } + + $phid_map[$user_phid] = id(new PhutilSortVector()) + ->addInt($is_viewer ? 0 : 1) + ->addInt($is_enabled ? 0 : 1) + ->addInt($order_scalar * count($phid_map)); + } + $phid_map = msortv($phid_map, 'getSelf'); + + $handles = iterator_to_array($handles); + $handles = array_select_keys($handles, array_keys($phid_map)); + + if ($limit) { + $handles = array_slice($handles, 0, $limit); + } + + foreach ($handles as $user_phid => $handle) { $item = id(new PHUIObjectItemView()) ->setHeader($handle->getFullName()) ->setHref($handle->getURI()) ->setImageURI($handle->getImageURI()); - $icon = id(new PHUIIconView()) - ->setIcon($handle->getIcon()); + if ($handle->isDisabled()) { + if ($is_panel) { + // Don't show disabled users in the panel view at all. + continue; + } - $subtitle = $handle->getSubtitle(); + $item + ->setDisabled(true) + ->addAttribute(pht('Disabled')); + } else { + $icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); - $item->addAttribute(array($icon, ' ', $subtitle)); + $subtitle = $handle->getSubtitle(); - if ($can_edit && !$limit) { + $item->addAttribute(array($icon, ' ', $subtitle)); + } + + if ($supports_edit && !$is_panel) { $remove_uri = $this->getRemoveURI($user_phid); $item->addAction( @@ -107,6 +133,7 @@ abstract class PhabricatorProjectUserListView extends AphrontView { ->setIcon('fa-times') ->setName(pht('Remove')) ->setHref($remove_uri) + ->setDisabled(!$can_edit) ->setWorkflow(true)); } @@ -128,14 +155,9 @@ abstract class PhabricatorProjectUserListView extends AphrontView { ->setHeader($header_text); if ($limit) { - $header->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon( - id(new PHUIIconView()) - ->setIcon('fa-list-ul')) - ->setText(pht('View All')) - ->setHref("/project/members/{$id}/")); + $list->newTailButton() + ->setText(pht('View All')) + ->setHref("/project/members/{$id}/"); } $box = id(new PHUIObjectBoxView()) From 8ff0e3ab351f5e806c1a9e072695e4f905604913 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Oct 2019 11:36:43 -0700 Subject: [PATCH 07/57] Support rich diff rendering with DocumentEngine for added/removed files Summary: Ref T13425. When a file (like a Jupyter notebook) is added or removed, we can still render a useful line-by-line diff. Test Plan: - Viewed add/modify/remove of Jupyter, source code, and images in 2up/1up mode, everything looked okay. Maniphest Tasks: T13425 Differential Revision: https://secure.phabricator.com/D20865 --- .../parser/DifferentialChangesetParser.php | 104 ++++++++++++------ .../DifferentialChangesetOneUpRenderer.php | 10 ++ .../diff/PhabricatorDocumentEngineBlocks.php | 5 +- .../document/PhabricatorDocumentEngine.php | 4 +- .../PhabricatorImageDocumentEngine.php | 33 ++++-- .../PhabricatorJupyterDocumentEngine.php | 21 +++- 6 files changed, 124 insertions(+), 53 deletions(-) diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 6c4ed3d14f..571405f64b 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1686,47 +1686,79 @@ final class DifferentialChangesetParser extends Phobject { break; } - $old_ref = id(new PhabricatorDocumentRef()) - ->setName($changeset->getOldFile()); - if ($old_file) { - $old_ref->setFile($old_file); + $type_delete = DifferentialChangeType::TYPE_DELETE; + $type_add = DifferentialChangeType::TYPE_ADD; + $change_type = $changeset->getChangeType(); + + $no_old = ($change_type == $type_add); + $no_new = ($change_type == $type_delete); + + if ($no_old) { + $old_ref = null; } else { - $old_data = $this->old; - $old_data = ipull($old_data, 'text'); - $old_data = implode('', $old_data); + $old_ref = id(new PhabricatorDocumentRef()) + ->setName($changeset->getOldFile()); + if ($old_file) { + $old_ref->setFile($old_file); + } else { + $old_data = $this->old; + $old_data = ipull($old_data, 'text'); + $old_data = implode('', $old_data); - $old_ref->setData($old_data); - } - - $new_ref = id(new PhabricatorDocumentRef()) - ->setName($changeset->getFilename()); - if ($new_file) { - $new_ref->setFile($new_file); - } else { - $new_data = $this->new; - $new_data = ipull($new_data, 'text'); - $new_data = implode('', $new_data); - - $new_ref->setData($new_data); - } - - $old_engines = PhabricatorDocumentEngine::getEnginesForRef( - $viewer, - $old_ref); - - $new_engines = PhabricatorDocumentEngine::getEnginesForRef( - $viewer, - $new_ref); - - $shared_engines = array_intersect_key($new_engines, $old_engines); - $default_engine = head_key($new_engines); - - foreach ($shared_engines as $key => $shared_engine) { - if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) { - unset($shared_engines[$key]); + $old_ref->setData($old_data); } } + if ($no_new) { + $new_ref = null; + } else { + $new_ref = id(new PhabricatorDocumentRef()) + ->setName($changeset->getFilename()); + if ($new_file) { + $new_ref->setFile($new_file); + } else { + $new_data = $this->new; + $new_data = ipull($new_data, 'text'); + $new_data = implode('', $new_data); + + $new_ref->setData($new_data); + } + } + + + $old_engines = null; + if ($old_ref) { + $old_engines = PhabricatorDocumentEngine::getEnginesForRef( + $viewer, + $old_ref); + } + + $new_engines = null; + if ($new_ref) { + $new_engines = PhabricatorDocumentEngine::getEnginesForRef( + $viewer, + $new_ref); + } + + if ($new_engines !== null && $old_engines !== null) { + $shared_engines = array_intersect_key($new_engines, $old_engines); + $default_engine = head_key($new_engines); + + foreach ($shared_engines as $key => $shared_engine) { + if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) { + unset($shared_engines[$key]); + } + } + } else if ($new_engines !== null) { + $shared_engines = $new_engines; + $default_engine = head_key($shared_engines); + } else if ($old_engines !== null) { + $shared_engines = $old_engines; + $default_engine = head_key($shared_engines); + } else { + return null; + } + $engine_key = $this->getDocumentEngineKey(); if (strlen($engine_key)) { if (isset($shared_engines[$engine_key])) { diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index fc50b0d605..19c939274d 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -371,17 +371,27 @@ final class DifferentialChangesetOneUpRenderer $cell_classes = $block_diff->getNewClasses(); } } else if ($row_type === 'old') { + if (!$old_ref) { + continue; + } + $cell_content = $engine->newBlockContentView( $old_ref, $old); + $cell_classes[] = 'old'; $cell_classes[] = 'old-full'; $new_key = null; } else if ($row_type === 'new') { + if (!$new_ref) { + continue; + } + $cell_content = $engine->newBlockContentView( $new_ref, $new); + $cell_classes[] = 'new'; $cell_classes[] = 'new-full'; diff --git a/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php b/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php index f8f3a414b1..47ddf194ee 100644 --- a/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php +++ b/src/applications/files/diff/PhabricatorDocumentEngineBlocks.php @@ -15,7 +15,10 @@ final class PhabricatorDocumentEngineBlocks return $this->messages; } - public function addBlockList(PhabricatorDocumentRef $ref, array $blocks) { + public function addBlockList( + PhabricatorDocumentRef $ref = null, + array $blocks = array()) { + assert_instances_of($blocks, 'PhabricatorDocumentEngineBlock'); $this->lists[] = array( diff --git a/src/applications/files/document/PhabricatorDocumentEngine.php b/src/applications/files/document/PhabricatorDocumentEngine.php index bc7960b4ad..9593e1d98d 100644 --- a/src/applications/files/document/PhabricatorDocumentEngine.php +++ b/src/applications/files/document/PhabricatorDocumentEngine.php @@ -32,8 +32,8 @@ abstract class PhabricatorDocumentEngine } public function canDiffDocuments( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { return false; } diff --git a/src/applications/files/document/PhabricatorImageDocumentEngine.php b/src/applications/files/document/PhabricatorImageDocumentEngine.php index fa678cc034..449d604370 100644 --- a/src/applications/files/document/PhabricatorImageDocumentEngine.php +++ b/src/applications/files/document/PhabricatorImageDocumentEngine.php @@ -18,21 +18,38 @@ final class PhabricatorImageDocumentEngine } public function canDiffDocuments( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { - // For now, we can only render a rich image diff if both documents have + // For now, we can only render a rich image diff if the documents have // their data stored in Files already. - return ($uref->getFile() && $vref->getFile()); + if ($uref && !$uref->getFile()) { + return false; + } + + if ($vref && !$vref->getFile()) { + return false; + } + + return true; } public function newEngineBlocks( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { - $u_blocks = $this->newDiffBlocks($uref); - $v_blocks = $this->newDiffBlocks($vref); + if ($uref) { + $u_blocks = $this->newDiffBlocks($uref); + } else { + $u_blocks = array(); + } + + if ($vref) { + $v_blocks = $this->newDiffBlocks($vref); + } else { + $v_blocks = array(); + } return id(new PhabricatorDocumentEngineBlocks()) ->addBlockList($uref, $u_blocks) diff --git a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php index d9d4c43abb..0c3e891c3c 100644 --- a/src/applications/files/document/PhabricatorJupyterDocumentEngine.php +++ b/src/applications/files/document/PhabricatorJupyterDocumentEngine.php @@ -36,20 +36,29 @@ final class PhabricatorJupyterDocumentEngine } public function canDiffDocuments( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { return true; } public function newEngineBlocks( - PhabricatorDocumentRef $uref, - PhabricatorDocumentRef $vref) { + PhabricatorDocumentRef $uref = null, + PhabricatorDocumentRef $vref = null) { $blocks = new PhabricatorDocumentEngineBlocks(); try { - $u_blocks = $this->newDiffBlocks($uref); - $v_blocks = $this->newDiffBlocks($vref); + if ($uref) { + $u_blocks = $this->newDiffBlocks($uref); + } else { + $u_blocks = array(); + } + + if ($vref) { + $v_blocks = $this->newDiffBlocks($vref); + } else { + $v_blocks = array(); + } $blocks->addBlockList($uref, $u_blocks); $blocks->addBlockList($vref, $v_blocks); From 292f8fc612bd2d070110eb22877454937bfd7899 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 26 Oct 2019 12:06:45 -0700 Subject: [PATCH 08/57] Fix an issue where added or removed source files could incorrectly select a DocumentEngine Summary: Ref T13425. The changes in D20865 could incorrectly lead to selection of a DocumentEngine that can not generate document diffs if a file was added or removed (for example, when a source file is added). Move the engine pruning code to be shared -- we should always discard engines which can't generate a diff, even if we don't have both documents. Test Plan: Viewed an added source file, no more document ref error arising from document engine selection. Maniphest Tasks: T13425 Differential Revision: https://secure.phabricator.com/D20866 --- .../parser/DifferentialChangesetParser.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 571405f64b..92d5f23c1a 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1743,12 +1743,6 @@ final class DifferentialChangesetParser extends Phobject { if ($new_engines !== null && $old_engines !== null) { $shared_engines = array_intersect_key($new_engines, $old_engines); $default_engine = head_key($new_engines); - - foreach ($shared_engines as $key => $shared_engine) { - if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) { - unset($shared_engines[$key]); - } - } } else if ($new_engines !== null) { $shared_engines = $new_engines; $default_engine = head_key($shared_engines); @@ -1759,6 +1753,12 @@ final class DifferentialChangesetParser extends Phobject { return null; } + foreach ($shared_engines as $key => $shared_engine) { + if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) { + unset($shared_engines[$key]); + } + } + $engine_key = $this->getDocumentEngineKey(); if (strlen($engine_key)) { if (isset($shared_engines[$engine_key])) { From 5d8457a07ee35612a55d744aece7665061ae6f4c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 28 Oct 2019 14:22:07 -0700 Subject: [PATCH 09/57] In the repository URI index, store Phabricator's own URIs as tokens Summary: Fixes T13435. If you move Phabricator or copy data from one environment to another, the repository URI index currently still references the old URI, since it writes the URI as a plain string. This may make "arc which" and similar workflows have difficulty identifying repositories. Instead, store the "phabricator.base-uri" domain and the "diffusion.ssh-host" domain as tokens, so lookups continue to work correctly even after these values change. Test Plan: - Added unit tests to cover the normalization. - Ran migration, ran daemons, inspected `repository_uriindex` table, saw a mixture of sensible tokens (for local domains) and static domains (like "github.com"). - Ran this thing: ``` $ echo '{"remoteURIs": ["ssh://git@local.phacility.com/diffusion/P"]}' | ./bin/conduit call --method repository.query --trace --input - Reading input from stdin... >>> [2] (+0) repository.query() >>> [3] (+3) local_repository <<< [3] (+3) 555 us >>> [4] (+5) SELECT `r`.* FROM `repository` `r` LEFT JOIN `local_repository`.`repository_uriindex` uri ON r.phid = uri.repositoryPHID WHERE (uri.repositoryURI IN ('/diffusion/P')) GROUP BY `r`.phid ORDER BY `r`.`id` DESC LIMIT 101 <<< [4] (+5) 596 us <<< [2] (+6) 6,108 us { "result": [ { "id": "1", "name": "Phabricator", "phid": "PHID-REPO-2psrynlauicce7d3q7g2", "callsign": "P", "monogram": "rP", "vcs": "git", "uri": "http://local.phacility.com/source/phabricator/", "remoteURI": "https://github.com/phacility/phabricator.git", "description": "asdf", "isActive": true, "isHosted": false, "isImporting": false, "encoding": "UTF-8", "staging": { "supported": true, "prefix": "phabricator", "uri": null } } ] } ``` Note the `WHERE` clause in the query normalizes the URI into "", and the lookup succeeds. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13435 Differential Revision: https://secure.phabricator.com/D20872 --- .../20191028.uriindex.01.rebuild.php | 4 +++ .../PhabricatorRepositoryURINormalizer.php | 29 ++++++++++++++++-- ...ricatorRepositoryURINormalizerTestCase.php | 30 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 resources/sql/autopatches/20191028.uriindex.01.rebuild.php diff --git a/resources/sql/autopatches/20191028.uriindex.01.rebuild.php b/resources/sql/autopatches/20191028.uriindex.01.rebuild.php new file mode 100644 index 0000000000..c9bd3d97ca --- /dev/null +++ b/resources/sql/autopatches/20191028.uriindex.01.rebuild.php @@ -0,0 +1,4 @@ +getDomain(); if (!strlen($domain)) { - $domain = ''; + return ''; } - return phutil_utf8_strtolower($domain); + $domain = phutil_utf8_strtolower($domain); + + // See T13435. If the domain for a repository URI is same as the install + // base URI, store it as a "" token instead of the actual domain + // so that the index does not fall out of date if the install moves. + + $base_uri = PhabricatorEnv::getURI('/'); + $base_uri = new PhutilURI($base_uri); + $base_domain = $base_uri->getDomain(); + $base_domain = phutil_utf8_strtolower($base_domain); + if ($domain === $base_domain) { + return ''; + } + + // Likewise, store a token for the "SSH Host" domain so it can be changed + // without requiring an index rebuild. + + $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host'); + if (strlen($ssh_host)) { + $ssh_host = phutil_utf8_strtolower($ssh_host); + if ($domain === $ssh_host) { + return ''; + } + } + + return $domain; } diff --git a/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php b/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php index 81dd735562..8ab54a23a4 100644 --- a/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php +++ b/src/applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php @@ -31,6 +31,36 @@ final class PhabricatorRepositoryURINormalizerTestCase } } + public function testDomainURINormalizer() { + $base_domain = 'base.phabricator.example.com'; + $ssh_domain = 'ssh.phabricator.example.com'; + + $env = PhabricatorEnv::beginScopedEnv(); + $env->overrideEnvConfig('phabricator.base-uri', 'http://'.$base_domain); + $env->overrideEnvConfig('diffusion.ssh-host', $ssh_domain); + + $cases = array( + '/' => '', + '/path/to/local/repo.git' => '', + 'ssh://user@domain.com/path.git' => 'domain.com', + 'ssh://user@DOMAIN.COM/path.git' => 'domain.com', + 'http://'.$base_domain.'/diffusion/X/' => '', + 'ssh://'.$ssh_domain.'/diffusion/X/' => '', + 'git@'.$ssh_domain.':bananas.git' => '', + ); + + $type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; + + foreach ($cases as $input => $expect) { + $normal = new PhabricatorRepositoryURINormalizer($type_git, $input); + + $this->assertEqual( + $expect, + $normal->getNormalizedDomain(), + pht('Normalized domain for "%s".', $input)); + } + } + public function testSVNURINormalizer() { $cases = array( 'file:///path/to/repo' => 'path/to/repo', From 02f85f03bda713f5d47e2114ef843da46945b608 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 13 Sep 2019 07:47:51 -0700 Subject: [PATCH 10/57] Remove the "ssh-auth-key" script Summary: Ref T13436. Historically, this script could be used with a forked copy of "sshd" to do lower-cost per-key auth. Relatively modern "sshd" supports "%f" to "AuthorizedKeysCommand", which effectively moots this. Users have never been instructed to use this script for anything, and we moved away from this specific patch to "sshd" some time ago. Test Plan: Grepped for "ssh-auth-key", no hits. Maniphest Tasks: T13436 Differential Revision: https://secure.phabricator.com/D20873 --- bin/ssh-auth-key | 1 - scripts/ssh/ssh-auth-key.php | 42 ------------------------------------ 2 files changed, 43 deletions(-) delete mode 120000 bin/ssh-auth-key delete mode 100755 scripts/ssh/ssh-auth-key.php diff --git a/bin/ssh-auth-key b/bin/ssh-auth-key deleted file mode 120000 index 7dff83c316..0000000000 --- a/bin/ssh-auth-key +++ /dev/null @@ -1 +0,0 @@ -../scripts/ssh/ssh-auth-key.php \ No newline at end of file diff --git a/scripts/ssh/ssh-auth-key.php b/scripts/ssh/ssh-auth-key.php deleted file mode 100755 index 0c23a20edf..0000000000 --- a/scripts/ssh/ssh-auth-key.php +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env php -setViewer(PhabricatorUser::getOmnipotentUser()) - ->withKeys(array($public_key)) - ->withIsActive(true) - ->executeOne(); -if (!$key) { - exit(1); -} - -$object = $key->getObject(); -if (!($object instanceof PhabricatorUser)) { - exit(1); -} - -$bin = $root.'/bin/ssh-exec'; -$cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $object->getUsername()); -// This is additional escaping for the SSH 'command="..."' string. -$cmd = addcslashes($cmd, '"\\'); - -$options = array( - 'command="'.$cmd.'"', - 'no-port-forwarding', - 'no-X11-forwarding', - 'no-agent-forwarding', - 'no-pty', -); - -echo implode(',', $options); -exit(0); From 24f771c1bc079caa3c7e75b63c5023000c332095 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 13 Sep 2019 08:25:22 -0700 Subject: [PATCH 11/57] Add an optional "--sshd-key" argument to "bin/ssh-auth" for reading "%k" from modern sshd Summary: Depends on D20873. Ref T13436. Allow callers to configure "bin/ssh-auth --sshd-key %k" as an "AuthorizedKeysCommand"; if they do, and we recognize the key, emit just that key in the output. Test Plan: - Used `git pull` locally, still worked fine. - Instrumented things, saw the public key lookup actually work and emit a single key. - Ran without "--sshd-key", got a full key list as before. Maniphest Tasks: T13436 Differential Revision: https://secure.phabricator.com/D20874 --- scripts/ssh/ssh-auth.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php index 3c4f3f2b33..2a27329d4a 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth.php @@ -4,6 +4,24 @@ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/init/init-script.php'; +// TODO: For now, this is using "parseParital()", not "parse()". This allows +// the script to accept (and ignore) additional arguments. This preserves +// backward compatibility until installs have time to migrate to the new +// syntax. + +$args = id(new PhutilArgumentParser($argv)) + ->parsePartial( + array( + array( + 'name' => 'sshd-key', + 'param' => 'k', + 'help' => pht( + 'Accepts the "%%k" parameter from "AuthorizedKeysCommand".'), + ), + )); + +$sshd_key = $args->getArg('sshd-key'); + // NOTE: We are caching a datastructure rather than the flat key file because // the path on disk to "ssh-exec" is arbitrarily mutable at runtime. See T12397. @@ -85,6 +103,22 @@ if ($authstruct === null) { $cache->setKey($authstruct_key, $authstruct_raw, $ttl); } +// If we've received an "--sshd-key" argument and it matches some known key, +// only emit that key. (For now, if the key doesn't match, we'll fall back to +// emitting all keys.) +if ($sshd_key !== null) { + $matches = array(); + foreach ($authstruct['keys'] as $key => $key_struct) { + if (phutil_hashes_are_identical($key_struct['key'], $sshd_key)) { + $matches[$key] = $key_struct; + } + } + + if ($matches) { + $authstruct['keys'] = $matches; + } +} + $bin = $root.'/bin/ssh-exec'; $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); From 4a53fc339e323b4a415996db5c57940f2530c5f3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 28 Oct 2019 18:28:11 -0700 Subject: [PATCH 12/57] Don't use "phutil_hashes_are_identical()" to compare public keys Summary: Ref T13436. There's no real security value to doing this comparison, it just wards off evil "security researchers" who get upset if you ever compare two strings with a non-constant-time algorithm. In practice, SSH public keys are pretty long, pretty public, and have pretty similar lengths. This leads to a relatively large amount of work to do constant-time comparisons on them (we frequently can't abort early after identifying differing string length). Test Plan: Ran `bin/ssh-auth --sshd-key ...` on `secure` with ~1K keys, saw runtime drop by ~50% (~400ms to ~200ms) with `===`. Maniphest Tasks: T13436 Differential Revision: https://secure.phabricator.com/D20875 --- scripts/ssh/ssh-auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ssh/ssh-auth.php b/scripts/ssh/ssh-auth.php index 2a27329d4a..19b1cc46b4 100755 --- a/scripts/ssh/ssh-auth.php +++ b/scripts/ssh/ssh-auth.php @@ -109,7 +109,7 @@ if ($authstruct === null) { if ($sshd_key !== null) { $matches = array(); foreach ($authstruct['keys'] as $key => $key_struct) { - if (phutil_hashes_are_identical($key_struct['key'], $sshd_key)) { + if ($key_struct['key'] === $sshd_key) { $matches[$key] = $key_struct; } } From e1da1d86d68021f7e69191e72f27e4293d2a0fe4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 29 Oct 2019 09:37:22 -0700 Subject: [PATCH 13/57] Trim and URI encode symbol names before building URIs from them Summary: Fixes T13437. This URI construction was just missing URI encoding. Also, trim the symbol because my test case ended up catching "#define\n" as symbol text. Test Plan: - Configured a repository to have PHP symbols. - Touched a ".php" file with "#define" in it. - Diffed the change. - Command-clicked "#define" in the UI, in Safari/MacOS, to jump to the definition. - Before: taken to a nonsense page where "#define" became an anchor. - After: taken to symbol search for "#define". Maniphest Tasks: T13437 Differential Revision: https://secure.phabricator.com/D20876 --- resources/celerity/map.php | 18 +++++++++--------- .../repository/repository-crossreference.js | 11 ++++++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0e53737aaf..19d74a3f52 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -12,7 +12,7 @@ return array( 'core.pkg.css' => '686ae87c', 'core.pkg.js' => '6e5c894f', 'differential.pkg.css' => '607c84be', - 'differential.pkg.js' => 'a0212a0b', + 'differential.pkg.js' => '1b97518d', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => 'a98c0bf7', 'maniphest.pkg.css' => '35995d6d', @@ -428,7 +428,7 @@ return array( 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', 'rsrc/js/application/releeph/releeph-request-state-change.js' => '9f081f05', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'aa3a100c', - 'rsrc/js/application/repository/repository-crossreference.js' => 'c15122b4', + 'rsrc/js/application/repository/repository-crossreference.js' => '1c95ea63', 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e5bdb730', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'b86f297f', 'rsrc/js/application/transactions/behavior-comment-actions.js' => '4dffaeb2', @@ -682,7 +682,7 @@ return array( 'javelin-behavior-reorder-applications' => 'aa371860', 'javelin-behavior-reorder-columns' => '8ac32fd9', 'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730', - 'javelin-behavior-repository-crossreference' => 'c15122b4', + 'javelin-behavior-repository-crossreference' => '1c95ea63', 'javelin-behavior-scrollbar' => '92388bae', 'javelin-behavior-search-reorder-queries' => 'b86f297f', 'javelin-behavior-select-content' => 'e8240b50', @@ -1034,6 +1034,12 @@ return array( 'javelin-install', 'javelin-util', ), + '1c95ea63' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-uri', + ), '1cab0e9a' => array( 'javelin-behavior', 'javelin-dom', @@ -1977,12 +1983,6 @@ return array( 'c03f2fb4' => array( 'javelin-install', ), - 'c15122b4' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-uri', - ), 'c2c500a7' => array( 'javelin-install', 'javelin-dom', diff --git a/webroot/rsrc/js/application/repository/repository-crossreference.js b/webroot/rsrc/js/application/repository/repository-crossreference.js index d6ff2a06aa..ba522d5b47 100644 --- a/webroot/rsrc/js/application/repository/repository-crossreference.js +++ b/webroot/rsrc/js/application/repository/repository-crossreference.js @@ -152,7 +152,16 @@ JX.behavior('repository-crossreference', function(config, statics) { query.char = char; } - var uri = JX.$U('/diffusion/symbol/' + symbol + '/'); + var uri_symbol = symbol; + + // In some cases, lexers may include whitespace in symbol tags. Trim it, + // since symbols with semantic whitespace aren't supported. + uri_symbol = uri_symbol.trim(); + + // See T13437. Symbols like "#define" need to be encoded. + uri_symbol = encodeURIComponent(uri_symbol); + + var uri = JX.$U('/diffusion/symbol/' + uri_symbol + '/'); uri.addQueryParams(query); window.open(uri.toString()); From 114166dd3261326a46e17651aef7d1a2744e7056 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 29 Oct 2019 13:13:59 -0700 Subject: [PATCH 14/57] Roughly implement "harbormaster.artifact.search" Summary: Ref T13438. This is a sort of minimal plausible implementation. Test Plan: Used "harbormaster.artifact.search" to query information about artifacts. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13438 Differential Revision: https://secure.phabricator.com/D20878 --- src/__phutil_library_map__.php | 5 + ...ormasterArtifactSearchConduitAPIMethod.php | 18 ++++ .../HarbormasterArtifactSearchEngine.php | 93 +++++++++++++++++++ .../build/HarbormasterBuildArtifact.php | 48 +++++++++- 4 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php create mode 100644 src/applications/harbormaster/query/HarbormasterArtifactSearchEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9d51d92760..83954637ce 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1338,6 +1338,8 @@ phutil_register_library_map(array( 'HarbormasterArcLintBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcLintBuildStepImplementation.php', 'HarbormasterArcUnitBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterArcUnitBuildStepImplementation.php', 'HarbormasterArtifact' => 'applications/harbormaster/artifact/HarbormasterArtifact.php', + 'HarbormasterArtifactSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php', + 'HarbormasterArtifactSearchEngine' => 'applications/harbormaster/query/HarbormasterArtifactSearchEngine.php', 'HarbormasterAutotargetsTestCase' => 'applications/harbormaster/__tests__/HarbormasterAutotargetsTestCase.php', 'HarbormasterBuild' => 'applications/harbormaster/storage/build/HarbormasterBuild.php', 'HarbormasterBuildAbortedException' => 'applications/harbormaster/exception/HarbormasterBuildAbortedException.php', @@ -7369,6 +7371,8 @@ phutil_register_library_map(array( 'HarbormasterArcLintBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArcUnitBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterArtifact' => 'Phobject', + 'HarbormasterArtifactSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'HarbormasterArtifactSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HarbormasterAutotargetsTestCase' => 'PhabricatorTestCase', 'HarbormasterBuild' => array( 'HarbormasterDAO', @@ -7384,6 +7388,7 @@ phutil_register_library_map(array( 'HarbormasterDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorConduitResultInterface', ), 'HarbormasterBuildArtifactPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php new file mode 100644 index 0000000000..63cba16af4 --- /dev/null +++ b/src/applications/harbormaster/conduit/HarbormasterArtifactSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +setLabel(pht('Targets')) + ->setKey('buildTargetPHIDs') + ->setAliases( + array( + 'buildTargetPHID', + 'buildTargets', + 'buildTarget', + 'targetPHIDs', + 'targetPHID', + 'targets', + 'target', + )) + ->setDescription( + pht('Search for artifacts attached to particular build targets.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['buildTargetPHIDs']) { + $query->withBuildTargetPHIDs($map['buildTargetPHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/harbormaster/artifact/'.$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Artifacts'), + ); + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $artifacts, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($artifacts, 'HarbormasterBuildArtifact'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + foreach ($artifacts as $artifact) { + $id = $artifact->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Artifact %d', $id)); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No artifacts found.')); + } + +} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php index 7cd8d60b6a..8b4972c154 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -4,7 +4,8 @@ final class HarbormasterBuildArtifact extends HarbormasterDAO implements PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorConduitResultInterface { protected $buildTargetPHID; protected $artifactType; @@ -18,6 +19,7 @@ final class HarbormasterBuildArtifact public static function initializeNewBuildArtifact( HarbormasterBuildTarget $build_target) { + return id(new HarbormasterBuildArtifact()) ->attachBuildTarget($build_target) ->setBuildTargetPHID($build_target->getPHID()); @@ -53,9 +55,8 @@ final class HarbormasterBuildArtifact ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID( - HarbormasterBuildArtifactPHIDType::TYPECONST); + public function getPHIDType() { + return HarbormasterBuildArtifactPHIDType::TYPECONST; } public function attachBuildTarget(HarbormasterBuildTarget $build_target) { @@ -147,7 +148,8 @@ final class HarbormasterBuildArtifact } public function describeAutomaticCapability($capability) { - return pht('Users must be able to see a buildable to see its artifacts.'); + return pht( + 'Users must be able to see a build target to see its artifacts.'); } @@ -165,4 +167,40 @@ final class HarbormasterBuildArtifact $this->saveTransaction(); } + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('buildTargetPHID') + ->setType('phid') + ->setDescription(pht('The build target this artifact is attached to.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('artifactType') + ->setType('string') + ->setDescription(pht('The artifact type.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('artifactKey') + ->setType('string') + ->setDescription(pht('The artifact key.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('isReleased') + ->setType('bool') + ->setDescription(pht('True if this artifact has been released.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'buildTargetPHID' => $this->getBuildTargetPHID(), + 'artifactType' => $this->getArtifactType(), + 'artifactKey' => $this->getArtifactKey(), + 'isReleased' => (bool)$this->getIsReleased(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } } From 9d8cdce8e1f2f34b4588969256a3e2c4aef9fafb Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 29 Oct 2019 13:43:57 -0700 Subject: [PATCH 15/57] Make the top-level burndown chart in "Maniphest > Reports" show open tasks, not total tasks Summary: Ref T13279. See PHI1491. Currently, the top-level "Burnup Rate" chart in Maniphest shows total created tasks above the X-axis, without adjusting for closures. This is unintended and not very useful. The filtered-by-project charts show the right value (cumulative open tasks, i.e. open minus close). Change the value to aggregate creation events and status change events. Test Plan: Viewed top-level chart, saw the value no longer monotonically increasing. Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20879 --- .../project/chart/PhabricatorProjectBurndownChartEngine.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php index 1296f2eec8..16760d515f 100644 --- a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -53,7 +53,11 @@ final class PhabricatorProjectBurndownChartEngine $open_function = $this->newFunction( array( 'accumulate', - array('fact', 'tasks.open-count.create'), + array( + 'sum', + array('fact', 'tasks.open-count.create'), + array('fact', 'tasks.open-count.status'), + ), )); $closed_function = $this->newFunction( From bcf15abcd33c77f1fc4d63b4c157b0fd3ccd1b05 Mon Sep 17 00:00:00 2001 From: Arturas Moskvinas Date: Mon, 21 Oct 2019 15:44:46 +0300 Subject: [PATCH 16/57] Return empty data if fact dimension is missing, not yet available Summary: On fresh installation which doesn't have yet any task closed you will not be able to open charts because of error below: Fixes: ``` [Mon Oct 21 15:42:41 2019] [2019-10-21 15:42:41] EXCEPTION: (TypeError) Argument 1 passed to head_key() must be of the type array, null given, called in ..phabricator/src/applications/fact/chart/PhabricatorFactChartFunction.php on line 86 at [/src/utils/utils.php:832] [Mon Oct 21 15:42:41 2019] #0 phlog(TypeError) called at [/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php:27] [Mon Oct 21 15:42:41 2019] #1 PhabricatorAjaxRequestExceptionHandler::handleRequestThrowable(AphrontRequest, TypeError) called at [/src/aphront/configuration/AphrontApplicationConfiguration.php:797] [Mon Oct 21 15:42:41 2019] #2 AphrontApplicationConfiguration::handleThrowable(TypeError) called at [/src/aphront/configuration/AphrontApplicationConfiguration.php:345] [Mon Oct 21 15:42:41 2019] #3 AphrontApplicationConfiguration::processRequest(AphrontRequest, PhutilDeferredLog, AphrontPHPHTTPSink, MultimeterControl) called at [/src/aphront/configuration/AphrontApplicationConfiguration.php:214] [Mon Oct 21 15:42:41 2019] #4 AphrontApplicationConfiguration::runHTTPRequest(AphrontPHPHTTPSink) called at [/webroot/index.php:35 ``` To fix issue - lets return empty data set instead Test Plan: 1) Create fresh phabricator installation 2) Create fresh project 3) Try viewing charts Reviewers: epriestley, Pawka, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, yelirekim Differential Revision: https://secure.phabricator.com/D20861 --- src/applications/fact/chart/PhabricatorFactChartFunction.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/fact/chart/PhabricatorFactChartFunction.php b/src/applications/fact/chart/PhabricatorFactChartFunction.php index 0e940d644d..1713282116 100644 --- a/src/applications/fact/chart/PhabricatorFactChartFunction.php +++ b/src/applications/fact/chart/PhabricatorFactChartFunction.php @@ -29,6 +29,7 @@ final class PhabricatorFactChartFunction $key_id = id(new PhabricatorFactKeyDimension()) ->newDimensionID($fact->getKey()); if (!$key_id) { + $this->map = array(); return; } From 97bed350857966458c39caac3c7b9a5e5f6eb2d6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 31 Oct 2019 08:50:31 -0700 Subject: [PATCH 17/57] Show repository information (and use repository identities) in commit hovercards Summary: Ref T12164. Ref T13439. Commit hovercards don't currently show the repository. Although this is sometimes obvious from context, it isn't at other times and it's clearly useful/important. Also, use identities to render author/committer information and show committer if the committer differs from the author. Test Plan: {F6989595} Maniphest Tasks: T13439, T12164 Differential Revision: https://secure.phabricator.com/D20881 --- .../DiffusionHovercardEngineExtension.php | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php index 3ce95cb3c3..cd0b3cceab 100644 --- a/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php +++ b/src/applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php @@ -26,20 +26,45 @@ final class DiffusionHovercardEngineExtension $viewer = $this->getViewer(); - $author_phid = $commit->getAuthorPHID(); - if ($author_phid) { - $author = $viewer->renderHandle($author_phid); - } else { - $commit_data = $commit->loadCommitData(); - $author = phutil_tag('em', array(), $commit_data->getAuthorName()); + $commit = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->needIdentities(true) + ->needCommitData(true) + ->withPHIDs(array($commit->getPHID())) + ->executeOne(); + if (!$commit) { + return; } + $author_phid = $commit->getAuthorDisplayPHID(); + $committer_phid = $commit->getCommitterDisplayPHID(); + $repository_phid = $commit->getRepository()->getPHID(); + + $phids = array(); + $phids[] = $author_phid; + $phids[] = $committer_phid; + $phids[] = $repository_phid; + + $handles = $viewer->loadHandles($phids); + $hovercard->setTitle($handle->getName()); $hovercard->setDetail($commit->getSummary()); - $hovercard->addField(pht('Author'), $author); - $hovercard->addField(pht('Date'), - phabricator_date($commit->getEpoch(), $viewer)); + $repository = $handles[$repository_phid]->renderLink(); + $hovercard->addField(pht('Repository'), $repository); + + $author = $handles[$author_phid]->renderLink(); + if ($author_phid) { + $hovercard->addField(pht('Author'), $author); + } + + if ($committer_phid && ($committer_phid !== $author_phid)) { + $committer = $handles[$committer_phid]->renderLink(); + $hovercard->addField(pht('Committer'), $committer); + } + + $date = phabricator_date($commit->getEpoch(), $viewer); + $hovercard->addField(pht('Date'), $date); if (!$commit->isAuditStatusNoAudit()) { $status = $commit->getAuditStatusObject(); From b0d9f89c953573a47cf2912765be07962b91d82d Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 31 Oct 2019 10:23:21 -0700 Subject: [PATCH 18/57] Remove "State Icons" from handles Summary: Ref T13440. This feature is used in only one interface which I'm about to rewrite, so throw it away. Test Plan: Grepped for all affected symbols, didn't find any hits anywhere. Maniphest Tasks: T13440 Differential Revision: https://secure.phabricator.com/D20882 --- .../phid/DifferentialRevisionPHIDType.php | 9 ---- .../ManiphestTaskDetailController.php | 6 +-- .../phid/PhabricatorObjectHandle.php | 53 ------------------- .../phid/view/PHUIHandleListView.php | 15 ------ src/applications/phid/view/PHUIHandleView.php | 15 ------ .../PhabricatorRepositoryCommitPHIDType.php | 10 ---- 6 files changed, 2 insertions(+), 106 deletions(-) diff --git a/src/applications/differential/phid/DifferentialRevisionPHIDType.php b/src/applications/differential/phid/DifferentialRevisionPHIDType.php index a117690d66..a7d3c9f4a7 100644 --- a/src/applications/differential/phid/DifferentialRevisionPHIDType.php +++ b/src/applications/differential/phid/DifferentialRevisionPHIDType.php @@ -44,15 +44,6 @@ final class DifferentialRevisionPHIDType extends PhabricatorPHIDType { if ($revision->isClosed()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); } - - $icon = $revision->getStatusIcon(); - $color = $revision->getStatusIconColor(); - $name = $revision->getStatusDisplayName(); - - $handle - ->setStateIcon($icon) - ->setStateColor($color) - ->setStateName($name); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index c5dba7d3b5..00b884d610 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -413,8 +413,7 @@ final class ManiphestTaskDetailController extends ManiphestController { foreach ($commit_phids as $phid) { $revisions_commits[$phid] = $handles->renderHandle($phid) - ->setShowHovercard(true) - ->setShowStateIcon(true); + ->setShowHovercard(true); $revision_phid = key($drev_edges[$phid][$commit_drev]); $revision_handle = $handles->getHandleIfExists($revision_phid); if ($revision_handle) { @@ -435,8 +434,7 @@ final class ManiphestTaskDetailController extends ManiphestController { $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); - $edge_list = $edge_handles->renderList() - ->setShowStateIcons(true); + $edge_list = $edge_handles->renderList(); $view->addProperty($edge_name, $edge_list); } diff --git a/src/applications/phid/PhabricatorObjectHandle.php b/src/applications/phid/PhabricatorObjectHandle.php index ba93dbcead..86f0f848c0 100644 --- a/src/applications/phid/PhabricatorObjectHandle.php +++ b/src/applications/phid/PhabricatorObjectHandle.php @@ -33,10 +33,6 @@ final class PhabricatorObjectHandle private $commandLineObjectName; private $mailStampName; - private $stateIcon; - private $stateColor; - private $stateName; - public function setIcon($icon) { $this->icon = $icon; return $this; @@ -299,55 +295,6 @@ final class PhabricatorObjectHandle return $this->complete; } - public function setStateIcon($state_icon) { - $this->stateIcon = $state_icon; - return $this; - } - - public function getStateIcon() { - return $this->stateIcon; - } - - public function setStateColor($state_color) { - $this->stateColor = $state_color; - return $this; - } - - public function getStateColor() { - return $this->stateColor; - } - - public function setStateName($state_name) { - $this->stateName = $state_name; - return $this; - } - - public function getStateName() { - return $this->stateName; - } - - public function renderStateIcon() { - $icon = $this->getStateIcon(); - if ($icon === null) { - $icon = 'fa-question-circle-o'; - } - - $color = $this->getStateColor(); - - $name = $this->getStateName(); - if ($name === null) { - $name = pht('Unknown'); - } - - return id(new PHUIIconView()) - ->setIcon($icon, $color) - ->addSigil('has-tooltip') - ->setMetadata( - array( - 'tip' => $name, - )); - } - public function renderLink($name = null) { return $this->renderLinkWithAttributes($name, array()); } diff --git a/src/applications/phid/view/PHUIHandleListView.php b/src/applications/phid/view/PHUIHandleListView.php index 24104fe76d..c5b2f19784 100644 --- a/src/applications/phid/view/PHUIHandleListView.php +++ b/src/applications/phid/view/PHUIHandleListView.php @@ -13,7 +13,6 @@ final class PHUIHandleListView private $handleList; private $asInline; private $asText; - private $showStateIcons; private $glyphLimit; public function setHandleList(PhabricatorHandleList $list) { @@ -39,15 +38,6 @@ final class PHUIHandleListView return $this->asText; } - public function setShowStateIcons($show_state_icons) { - $this->showStateIcons = $show_state_icons; - return $this; - } - - public function getShowStateIcons() { - return $this->showStateIcons; - } - public function setGlyphLimit($glyph_limit) { $this->glyphLimit = $glyph_limit; return $this; @@ -70,7 +60,6 @@ final class PHUIHandleListView protected function getTagContent() { $list = $this->handleList; - $show_state_icons = $this->getShowStateIcons(); $glyph_limit = $this->getGlyphLimit(); $items = array(); @@ -79,10 +68,6 @@ final class PHUIHandleListView ->setShowHovercard(true) ->setAsText($this->getAsText()); - if ($show_state_icons) { - $view->setShowStateIcon(true); - } - if ($glyph_limit) { $view->setGlyphLimit($glyph_limit); } diff --git a/src/applications/phid/view/PHUIHandleView.php b/src/applications/phid/view/PHUIHandleView.php index fe3c62a9ac..6cdf84f391 100644 --- a/src/applications/phid/view/PHUIHandleView.php +++ b/src/applications/phid/view/PHUIHandleView.php @@ -17,7 +17,6 @@ final class PHUIHandleView private $asText; private $useShortName; private $showHovercard; - private $showStateIcon; private $glyphLimit; public function setHandleList(PhabricatorHandleList $list) { @@ -50,15 +49,6 @@ final class PHUIHandleView return $this; } - public function setShowStateIcon($show_state_icon) { - $this->showStateIcon = $show_state_icon; - return $this; - } - - public function getShowStateIcon() { - return $this->showStateIcon; - } - public function setGlyphLimit($glyph_limit) { $this->glyphLimit = $glyph_limit; return $this; @@ -104,11 +94,6 @@ final class PHUIHandleView $link = $handle->renderLink($name); } - if ($this->showStateIcon) { - $icon = $handle->renderStateIcon(); - $link = array($icon, ' ', $link); - } - return $link; } diff --git a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php index c37bdc04f9..df84f2dcfd 100644 --- a/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php +++ b/src/applications/repository/phid/PhabricatorRepositoryCommitPHIDType.php @@ -81,16 +81,6 @@ final class PhabricatorRepositoryCommitPHIDType extends PhabricatorPHIDType { $handle->setFullName($full_name); $handle->setURI($commit->getURI()); $handle->setTimestamp($commit->getEpoch()); - - $status = $commit->getAuditStatusObject(); - $icon = $status->getIcon(); - $color = $status->getColor(); - $name = $status->getName(); - - $handle - ->setStateIcon($icon) - ->setStateColor($color) - ->setStateName($name); } } From 7bdfe5b46adaab3b87f43485deb659baab2e58b6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 31 Oct 2019 10:23:27 -0700 Subject: [PATCH 19/57] Show commits and revisions on tasks in a tabular view instead of handle lists Summary: Depends on D20882. Ref T13440. Instead of lists of "Differential Revisions" and "Commits", show all changes related to the task in a tabular view. Test Plan: {F6989816} Maniphest Tasks: T13440 Differential Revision: https://secure.phabricator.com/D20883 --- .../ManiphestTaskDetailController.php | 323 +++++++++++++++--- 1 file changed, 273 insertions(+), 50 deletions(-) diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 00b884d610..496b4f8cb2 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -179,11 +179,14 @@ final class ManiphestTaskDetailController extends ManiphestController { ->addTabGroup($tab_group); } + $changes_view = $this->newChangesView($task, $edges); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( + $changes_view, $tab_view, $timeline, $comment_view, @@ -395,56 +398,6 @@ final class ManiphestTaskDetailController extends ManiphestController { $source)); } - $edge_types = array( - ManiphestTaskHasRevisionEdgeType::EDGECONST - => pht('Differential Revisions'), - ); - - $revisions_commits = array(); - - $commit_phids = array_keys( - $edges[ManiphestTaskHasCommitEdgeType::EDGECONST]); - if ($commit_phids) { - $commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST; - $drev_edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($commit_phids) - ->withEdgeTypes(array($commit_drev)) - ->execute(); - - foreach ($commit_phids as $phid) { - $revisions_commits[$phid] = $handles->renderHandle($phid) - ->setShowHovercard(true); - $revision_phid = key($drev_edges[$phid][$commit_drev]); - $revision_handle = $handles->getHandleIfExists($revision_phid); - if ($revision_handle) { - $task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST; - unset($edges[$task_drev][$revision_phid]); - $revisions_commits[$phid] = hsprintf( - '%s / %s', - $revision_handle->renderHovercardLink($revision_handle->getName()), - $revisions_commits[$phid]); - } - } - } - - foreach ($edge_types as $edge_type => $edge_name) { - if (!$edges[$edge_type]) { - continue; - } - - $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); - - $edge_list = $edge_handles->renderList(); - - $view->addProperty($edge_name, $edge_list); - } - - if ($revisions_commits) { - $view->addProperty( - pht('Commits'), - phutil_implode_html(phutil_tag('br'), $revisions_commits)); - } - $field_list->appendFieldsToPropertyList( $task, $viewer, @@ -594,5 +547,275 @@ final class ManiphestTaskDetailController extends ManiphestController { return $handles->newSublist($phids); } + private function newChangesView(ManiphestTask $task, array $edges) { + $viewer = $this->getViewer(); + + $revision_type = ManiphestTaskHasRevisionEdgeType::EDGECONST; + $commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST; + + $revision_phids = idx($edges, $revision_type, array()); + $revision_phids = array_keys($revision_phids); + $revision_phids = array_fuse($revision_phids); + + $commit_phids = idx($edges, $commit_type, array()); + $commit_phids = array_keys($commit_phids); + $commit_phids = array_fuse($commit_phids); + + if (!$revision_phids && !$commit_phids) { + return null; + } + + if ($commit_phids) { + $link_type = DiffusionCommitHasRevisionEdgeType::EDGECONST; + $link_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($commit_phids) + ->withEdgeTypes(array($link_type)); + $link_query->execute(); + + $commits = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withPHIDs($commit_phids) + ->execute(); + $commits = mpull($commits, null, 'getPHID'); + } else { + $commits = array(); + } + + if ($revision_phids) { + $revisions = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withPHIDs($revision_phids) + ->execute(); + $revisions = mpull($revisions, null, 'getPHID'); + } else { + $revisions = array(); + } + + $handle_phids = array(); + $any_linked = false; + + $tail = array(); + foreach ($commit_phids as $commit_phid) { + $handle_phids[] = $commit_phid; + + $link_phids = $link_query->getDestinationPHIDs(array($commit_phid)); + foreach ($link_phids as $link_phid) { + $handle_phids[] = $link_phid; + unset($revision_phids[$link_phid]); + $any_linked = true; + } + + $commit = idx($commits, $commit_phid); + if ($commit) { + $repository_phid = $commit->getRepository()->getPHID(); + $handle_phids[] = $repository_phid; + } else { + $repository_phid = null; + } + + $status_view = null; + if ($commit) { + $status = $commit->getAuditStatusObject(); + if (!$status->isNoAudit()) { + $status_view = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setIcon($status->getIcon()) + ->setColor($status->getColor()) + ->setName($status->getName()); + + } + } + + $object_link = null; + if ($commit) { + $commit_monogram = $commit->getDisplayName(); + $commit_monogram = phutil_tag( + 'span', + array( + 'class' => 'object-name', + ), + $commit_monogram); + + $commit_link = javelin_tag( + 'a', + array( + 'href' => $commit->getURI(), + 'sigil' => 'hovercard', + 'meta' => array( + 'hoverPHID' => $commit->getPHID(), + ), + ), + $commit->getSummary()); + + $object_link = array( + $commit_monogram, + ' ', + $commit_link, + ); + } + + $tail[] = array( + 'objectPHID' => $commit_phid, + 'objectLink' => $object_link, + 'repositoryPHID' => $repository_phid, + 'revisionPHIDs' => $link_phids, + 'status' => $status_view, + ); + } + + $head = array(); + foreach ($revision_phids as $revision_phid) { + $handle_phids[] = $revision_phid; + + $revision = idx($revisions, $revision_phid); + if ($revision) { + $repository_phid = $revision->getRepositoryPHID(); + $handle_phids[] = $repository_phid; + } else { + $repository_phid = null; + } + + if ($revision) { + $icon = $revision->getStatusIcon(); + $color = $revision->getStatusIconColor(); + $name = $revision->getStatusDisplayName(); + + $status_view = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setIcon($icon) + ->setColor($color) + ->setName($name); + } else { + $status_view = null; + } + + $object_link = null; + if ($revision) { + $revision_monogram = $revision->getMonogram(); + $revision_monogram = phutil_tag( + 'span', + array( + 'class' => 'object-name', + ), + $revision_monogram); + + $revision_link = javelin_tag( + 'a', + array( + 'href' => $revision->getURI(), + 'sigil' => 'hovercard', + 'meta' => array( + 'hoverPHID' => $revision->getPHID(), + ), + ), + $revision->getTitle()); + + $object_link = array( + $revision_monogram, + ' ', + $revision_link, + ); + } + + $head[] = array( + 'objectPHID' => $revision_phid, + 'objectLink' => $object_link, + 'repositoryPHID' => $repository_phid, + 'revisionPHIDs' => array(), + 'status' => $status_view, + ); + } + + $objects = array_merge($head, $tail); + $handles = $viewer->loadHandles($handle_phids); + + $rows = array(); + foreach ($objects as $object) { + $object_phid = $object['objectPHID']; + $handle = $handles[$object_phid]; + + $object_link = $object['objectLink']; + if ($object_link === null) { + $object_link = $handle->renderLink(); + } + + $object_icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); + + $repository_link = null; + $repository_phid = $object['repositoryPHID']; + if ($repository_phid) { + $repository_link = $handles[$repository_phid]->renderLink(); + } + + $status_view = $object['status']; + + $revision_tags = array(); + foreach ($object['revisionPHIDs'] as $link_phid) { + $revision_handle = $handles[$link_phid]; + + $revision_name = $revision_handle->getName(); + $revision_tags[] = $revision_handle + ->renderHovercardLink($revision_name); + } + $revision_tags = phutil_implode_html( + phutil_tag('br'), + $revision_tags); + + $rows[] = array( + $object_icon, + $status_view, + $repository_link, + $revision_tags, + $object_link, + ); + } + + $changes_table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This task has no related commits or revisions.')) + ->setHeaders( + array( + null, + null, + pht('Repository'), + null, + pht('Revision/Commit'), + )) + ->setColumnClasses( + array( + 'center', + null, + null, + null, + 'wide pri object-link', + )) + ->setColumnVisibility( + array( + true, + true, + true, + $any_linked, + true, + )) + ->setDeviceVisibility( + array( + false, + true, + false, + false, + true, + )); + + $changes_header = id(new PHUIHeaderView()) + ->setHeader(pht('Revisions and Commits')); + + $changes_view = id(new PHUIObjectBoxView()) + ->setHeader($changes_header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($changes_table); + + return $changes_view; + } + } From c48f300eb16954ef7f254be55f8a3df777061422 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 31 Oct 2019 11:59:19 -0700 Subject: [PATCH 20/57] Add support for rendering section dividers in tables; use section dividers for changes on tasks Summary: Depends on D20883. Ref T13440. In most cases, all changes belong to the same repository, which makes the "Repository" column redundant and visually noisy. Show repository information in a section header. Test Plan: {F6989932} Maniphest Tasks: T13440 Differential Revision: https://secure.phabricator.com/D20884 --- resources/celerity/map.php | 6 +-- .../ManiphestTaskDetailController.php | 52 +++++++++++++------ src/view/control/AphrontTableView.php | 27 +++++++++- webroot/rsrc/css/aphront/table-view.css | 6 +++ 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 19d74a3f52..8793ab347c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '686ae87c', + 'core.pkg.css' => '9a391b14', 'core.pkg.js' => '6e5c894f', 'differential.pkg.css' => '607c84be', 'differential.pkg.js' => '1b97518d', @@ -30,7 +30,7 @@ return array( 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => '5f13a9e4', + 'rsrc/css/aphront/table-view.css' => '061e45eb', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', @@ -535,7 +535,7 @@ return array( 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => '5f13a9e4', + 'aphront-table-view-css' => '061e45eb', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 496b4f8cb2..f2a15e5605 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -594,7 +594,8 @@ final class ManiphestTaskDetailController extends ManiphestController { $handle_phids = array(); $any_linked = false; - $tail = array(); + $idx = 0; + $objects = array(); foreach ($commit_phids as $commit_phid) { $handle_phids[] = $commit_phid; @@ -654,16 +655,20 @@ final class ManiphestTaskDetailController extends ManiphestController { ); } - $tail[] = array( + $objects[] = array( 'objectPHID' => $commit_phid, 'objectLink' => $object_link, 'repositoryPHID' => $repository_phid, 'revisionPHIDs' => $link_phids, 'status' => $status_view, + 'order' => id(new PhutilSortVector()) + ->addInt($repository_phid ? 1 : 0) + ->addString((string)$repository_phid) + ->addInt(1) + ->addInt($idx++), ); } - $head = array(); foreach ($revision_phids as $revision_phid) { $handle_phids[] = $revision_phid; @@ -717,20 +722,44 @@ final class ManiphestTaskDetailController extends ManiphestController { ); } - $head[] = array( + $objects[] = array( 'objectPHID' => $revision_phid, 'objectLink' => $object_link, 'repositoryPHID' => $repository_phid, 'revisionPHIDs' => array(), 'status' => $status_view, + 'order' => id(new PhutilSortVector()) + ->addInt($repository_phid ? 1 : 0) + ->addString((string)$repository_phid) + ->addInt(0) + ->addInt($idx++), ); } - $objects = array_merge($head, $tail); $handles = $viewer->loadHandles($handle_phids); + $order = ipull($objects, 'order'); + $order = msortv($order, 'getSelf'); + $objects = array_select_keys($objects, array_keys($order)); + + $last_repository = false; $rows = array(); + $rowd = array(); foreach ($objects as $object) { + $repository_phid = $object['repositoryPHID']; + if ($repository_phid !== $last_repository) { + $repository_link = null; + if ($repository_phid) { + $repository_link = $handles[$repository_phid]->renderLink(); + $rows[] = array( + $repository_link, + ); + $rowd[] = true; + } + + $last_repository = $repository_phid; + } + $object_phid = $object['objectPHID']; $handle = $handles[$object_phid]; @@ -742,12 +771,6 @@ final class ManiphestTaskDetailController extends ManiphestController { $object_icon = id(new PHUIIconView()) ->setIcon($handle->getIcon()); - $repository_link = null; - $repository_phid = $object['repositoryPHID']; - if ($repository_phid) { - $repository_link = $handles[$repository_phid]->renderLink(); - } - $status_view = $object['status']; $revision_tags = array(); @@ -762,10 +785,10 @@ final class ManiphestTaskDetailController extends ManiphestController { phutil_tag('br'), $revision_tags); + $rowd[] = false; $rows[] = array( $object_icon, $status_view, - $repository_link, $revision_tags, $object_link, ); @@ -773,11 +796,11 @@ final class ManiphestTaskDetailController extends ManiphestController { $changes_table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This task has no related commits or revisions.')) + ->setRowDividers($rowd) ->setHeaders( array( null, null, - pht('Repository'), null, pht('Revision/Commit'), )) @@ -786,12 +809,10 @@ final class ManiphestTaskDetailController extends ManiphestController { 'center', null, null, - null, 'wide pri object-link', )) ->setColumnVisibility( array( - true, true, true, $any_linked, @@ -802,7 +823,6 @@ final class ManiphestTaskDetailController extends ManiphestController { false, true, false, - false, true, )); diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php index cae9dabec2..dff64169c2 100644 --- a/src/view/control/AphrontTableView.php +++ b/src/view/control/AphrontTableView.php @@ -24,6 +24,8 @@ final class AphrontTableView extends AphrontView { protected $sortValues = array(); private $deviceReadyTable; + private $rowDividers = array(); + public function __construct(array $data) { $this->data = $data; } @@ -53,6 +55,11 @@ final class AphrontTableView extends AphrontView { return $this; } + public function setRowDividers(array $dividers) { + $this->rowDividers = $dividers; + return $this; + } + public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; @@ -258,10 +265,15 @@ final class AphrontTableView extends AphrontView { } } + $dividers = $this->rowDividers; + $data = $this->data; if ($data) { $row_num = 0; + $row_idx = 0; foreach ($data as $row) { + $is_divider = !empty($dividers[$row_num]); + $row_size = count($row); while (count($row) > count($col_classes)) { $col_classes[] = null; @@ -289,6 +301,18 @@ final class AphrontTableView extends AphrontView { $class = trim($class.' '.$this->cellClasses[$row_num][$col_num]); } + if ($is_divider) { + $tr[] = phutil_tag( + 'td', + array( + 'class' => 'row-divider', + 'colspan' => count($headers), + ), + $value); + $row_idx = -1; + break; + } + $tr[] = phutil_tag( 'td', array( @@ -299,7 +323,7 @@ final class AphrontTableView extends AphrontView { } $class = idx($this->rowClasses, $row_num); - if ($this->zebraStripes && ($row_num % 2)) { + if ($this->zebraStripes && ($row_idx % 2)) { if ($class !== null) { $class = 'alt alt-'.$class; } else { @@ -309,6 +333,7 @@ final class AphrontTableView extends AphrontView { $table[] = phutil_tag('tr', array('class' => $class), $tr); ++$row_num; + ++$row_idx; } } else { $colspan = max(count(array_filter($visibility)), 1); diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 3736ffe841..c58e2ab2fc 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -55,6 +55,12 @@ background-color: {$lightbluebackground}; } +.aphront-table-view td.row-divider { + background-color: {$bluebackground}; + font-weight: bold; + padding: 8px 12px; +} + .aphront-table-view th { border-bottom: 1px solid {$thinblueborder}; } From e46e383bf25f42233302e1af7896f0971ea472aa Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 31 Oct 2019 12:26:23 -0700 Subject: [PATCH 21/57] Clean up "Revisions/Commits" table in Maniphest slightly Summary: Ref T13440. Give the table more obvious visual structure and get rid of the largely useless header columns. Test Plan: Viewed table, saw a slightly cleaner result. Maniphest Tasks: T13440 Differential Revision: https://secure.phabricator.com/D20885 --- resources/celerity/map.php | 6 ++--- .../ManiphestTaskDetailController.php | 22 ++++++++----------- src/view/control/AphrontTableView.php | 2 +- webroot/rsrc/css/aphront/table-view.css | 4 ++++ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8793ab347c..8547f8a0a3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '9a391b14', + 'core.pkg.css' => '77de226f', 'core.pkg.js' => '6e5c894f', 'differential.pkg.css' => '607c84be', 'differential.pkg.js' => '1b97518d', @@ -30,7 +30,7 @@ return array( 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => '061e45eb', + 'rsrc/css/aphront/table-view.css' => '0bb61df1', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', @@ -535,7 +535,7 @@ return array( 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => '061e45eb', + 'aphront-table-view-css' => '0bb61df1', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index f2a15e5605..b6985268db 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -593,6 +593,7 @@ final class ManiphestTaskDetailController extends ManiphestController { $handle_phids = array(); $any_linked = false; + $any_status = false; $idx = 0; $objects = array(); @@ -623,7 +624,6 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setIcon($status->getIcon()) ->setColor($status->getColor()) ->setName($status->getName()); - } } @@ -750,9 +750,9 @@ final class ManiphestTaskDetailController extends ManiphestController { if ($repository_phid !== $last_repository) { $repository_link = null; if ($repository_phid) { - $repository_link = $handles[$repository_phid]->renderLink(); + $repository_handle = $handles[$repository_phid]; $rows[] = array( - $repository_link, + $repository_handle->renderLink(), ); $rowd[] = true; } @@ -772,6 +772,9 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setIcon($handle->getIcon()); $status_view = $object['status']; + if ($status_view) { + $any_status = true; + } $revision_tags = array(); foreach ($object['revisionPHIDs'] as $link_phid) { @@ -797,16 +800,9 @@ final class ManiphestTaskDetailController extends ManiphestController { $changes_table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This task has no related commits or revisions.')) ->setRowDividers($rowd) - ->setHeaders( - array( - null, - null, - null, - pht('Revision/Commit'), - )) ->setColumnClasses( array( - 'center', + 'indent center', null, null, 'wide pri object-link', @@ -814,14 +810,14 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setColumnVisibility( array( true, - true, + $any_status, $any_linked, true, )) ->setDeviceVisibility( array( false, - true, + $any_status, false, true, )); diff --git a/src/view/control/AphrontTableView.php b/src/view/control/AphrontTableView.php index dff64169c2..a3c0a49be4 100644 --- a/src/view/control/AphrontTableView.php +++ b/src/view/control/AphrontTableView.php @@ -306,7 +306,7 @@ final class AphrontTableView extends AphrontView { 'td', array( 'class' => 'row-divider', - 'colspan' => count($headers), + 'colspan' => count($visibility), ), $value); $row_idx = -1; diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index c58e2ab2fc..e92f499634 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -61,6 +61,10 @@ padding: 8px 12px; } +.aphront-table-view td.indent { + padding-left: 24px; +} + .aphront-table-view th { border-bottom: 1px solid {$thinblueborder}; } From be2b8f4bcb62deb953049debacfbc7dca2e6edef Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 31 Oct 2019 12:43:40 -0700 Subject: [PATCH 22/57] Support querying projects by "Root Projects" in the UI, and "min/max depth" in the API Summary: Fixes T13441. Internally, projects can be queried by depth, but this is not exposed in the UI. Add a "Is root project?" contraint in the UI, and "minDepth" / "maxDepth" constraints to the API. Test Plan: - Used the UI to query root projects, got only root projects back. - Used "project.search" in the API to query combinations of root projects and projects at particular depths, got matching results. Maniphest Tasks: T13441 Differential Revision: https://secure.phabricator.com/D20886 --- src/__phutil_library_map__.php | 2 + .../query/PhabricatorProjectSearchEngine.php | 65 +++++++++++++++++++ .../field/PhabricatorSearchIntField.php | 22 +++++++ 3 files changed, 89 insertions(+) create mode 100644 src/applications/search/field/PhabricatorSearchIntField.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 83954637ce..2db41f0a69 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4652,6 +4652,7 @@ phutil_register_library_map(array( 'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php', 'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php', 'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'applications/search/engineextension/PhabricatorSearchIndexVersionDestructionEngineExtension.php', + 'PhabricatorSearchIntField' => 'applications/search/field/PhabricatorSearchIntField.php', 'PhabricatorSearchManagementIndexWorkflow' => 'applications/search/management/PhabricatorSearchManagementIndexWorkflow.php', 'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php', 'PhabricatorSearchManagementNgramsWorkflow' => 'applications/search/management/PhabricatorSearchManagementNgramsWorkflow.php', @@ -11273,6 +11274,7 @@ phutil_register_library_map(array( 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO', 'PhabricatorSearchIndexVersionDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', + 'PhabricatorSearchIntField' => 'PhabricatorSearchField', 'PhabricatorSearchManagementIndexWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow', 'PhabricatorSearchManagementNgramsWorkflow' => 'PhabricatorSearchManagementWorkflow', diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index 88a35676cc..cb179c995f 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -65,6 +65,35 @@ final class PhabricatorProjectSearchEngine pht( 'Pass true to find only milestones, or false to omit '. 'milestones.')), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Root Projects')) + ->setKey('isRoot') + ->setOptions( + pht('(Show All)'), + pht('Show Only Root Projects'), + pht('Hide Root Projects')) + ->setDescription( + pht( + 'Pass true to find only root projects, or false to omit '. + 'root projects.')), + id(new PhabricatorSearchIntField()) + ->setLabel(pht('Minimum Depth')) + ->setKey('minDepth') + ->setIsHidden(true) + ->setDescription( + pht( + 'Find projects with a given minimum depth. Root projects '. + 'have depth 0, their immediate children have depth 1, and '. + 'so on.')), + id(new PhabricatorSearchIntField()) + ->setLabel(pht('Maximum Depth')) + ->setKey('maxDepth') + ->setIsHidden(true) + ->setDescription( + pht( + 'Find projects with a given maximum depth. Root projects '. + 'have depth 0, their immediate children have depth 1, and '. + 'so on.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Subtypes')) ->setKey('subtypes') @@ -137,6 +166,42 @@ final class PhabricatorProjectSearchEngine $query->withIsMilestone($map['isMilestone']); } + $min_depth = $map['minDepth']; + $max_depth = $map['maxDepth']; + + if ($min_depth !== null || $max_depth !== null) { + if ($min_depth !== null && $max_depth !== null) { + if ($min_depth > $max_depth) { + throw new Exception( + pht( + 'Search constraint "minDepth" must be no larger than '. + 'search constraint "maxDepth".')); + } + } + } + + if ($map['isRoot'] !== null) { + if ($map['isRoot']) { + if ($max_depth === null) { + $max_depth = 0; + } else { + $max_depth = min(0, $max_depth); + } + + $query->withDepthBetween(null, 0); + } else { + if ($min_depth === null) { + $min_depth = 1; + } else { + $min_depth = max($min_depth, 1); + } + } + } + + if ($min_depth !== null || $max_depth !== null) { + $query->withDepthBetween($min_depth, $max_depth); + } + if ($map['parentPHIDs']) { $query->withParentProjectPHIDs($map['parentPHIDs']); } diff --git a/src/applications/search/field/PhabricatorSearchIntField.php b/src/applications/search/field/PhabricatorSearchIntField.php new file mode 100644 index 0000000000..70af934470 --- /dev/null +++ b/src/applications/search/field/PhabricatorSearchIntField.php @@ -0,0 +1,22 @@ +getInt($key); + } + + protected function newControl() { + return new AphrontFormTextControl(); + } + + protected function newConduitParameterType() { + return new ConduitIntParameterType(); + } + +} From 6bada7db4ceb83158d60b0ff1ff79554833f909b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Nov 2019 07:48:26 -0800 Subject: [PATCH 23/57] Change the Herald "do not include [any of]" condition label to "include none of" Summary: Fixes T13445. Make the meaning of this condition more clear, since the current wording is ambiguous between "any of" and "all of". Test Plan: Edited a Herald rule with a PHID list field ("Project tags"), saw text label say "include none of". Maniphest Tasks: T13445 Differential Revision: https://secure.phabricator.com/D20889 --- src/applications/herald/adapter/HeraldAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 2832c6d9f4..70f6e3d5cb 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -399,7 +399,7 @@ abstract class HeraldAdapter extends Phobject { self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), - self::CONDITION_INCLUDE_NONE => pht('do not include'), + self::CONDITION_INCLUDE_NONE => pht('include none of'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), From f5f2a0bc5696372945465b421d0026a9dd8b7a54 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Nov 2019 08:05:41 -0800 Subject: [PATCH 24/57] Allow username changes which modify letter case to go through as valid Summary: Fixes T13446. Currently, the validation logic here rejects a rename like "alice" to "ALICE" (which changes only letter case) but this is a permissible rename. Allow collisions that collide with the same user to permit this rename. Also, fix an issue where an empty rename was treated improperly. Test Plan: - Renamed "alice" to "ALICE". - Before: username collision error. - After: clean rename. - Renamed "alice" to "orange" (an existing user). Got an error. - Renamed "alice" to "", "!@#$", etc (invalid usernames). Got sensible errors. Maniphest Tasks: T13446 Differential Revision: https://secure.phabricator.com/D20890 --- .../PhabricatorUserUsernameTransaction.php | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index 338b296335..e30a131cff 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -71,21 +71,30 @@ final class PhabricatorUserUsernameTransaction } if (!strlen($new)) { - $errors[] = $this->newRequiredError( - pht('New username is required.'), $xaction); + $errors[] = $this->newInvalidError( + pht('New username is required.'), + $xaction); } else if (!PhabricatorUser::validateUsername($new)) { $errors[] = $this->newInvalidError( - PhabricatorUser::describeValidUsername(), $xaction); + PhabricatorUser::describeValidUsername(), + $xaction); } $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withUsernames(array($new)) ->executeOne(); - if ($user) { - $errors[] = $this->newInvalidError( - pht('Another user already has that username.'), $xaction); + // See T13446. We may be changing the letter case of a username, which + // is a perfectly fine edit. + $is_self = ($user->getPHID() === $object->getPHID()); + if (!$is_self) { + $errors[] = $this->newInvalidError( + pht( + 'Another user already has the username "%s".', + $new), + $xaction); + } } } From 9dbde24dda723959d43c79ea5069eb91db9024b8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Nov 2019 15:26:12 -0800 Subject: [PATCH 25/57] Manually prune Git working copy refs instead of using "--prune", to improve "Fetch Refs" behavior Summary: Ref T13448. When "Fetch Refs" is configured: - We switch to a narrow mode when running "ls-remote" against the local working copy. This excludes surplus refs, so we'll incorrectly detect that the local and remote working copies are identical in cases where the local working copy really has surplus refs. - We rely on "--prune" to remove surplus local refs, but it only prunes refs matching the refspecs we pass "git fetch". Since these refspecs are very narrow under "Fetch Only", the pruning behavior is also very narrow. Instead: - When listing local refs, always list everything. If we have too much stuff locally, we want to get rid of it. - When we identify surplus local refs, explicitly delete them instead of relying on "--prune". We can just do this in all cases so we don't have separate "--prune" and "manual" cases. Test Plan: - Created a new repository, observed from a GitHub repository, with many tags/refs/branches. Pulled it. - Observed lots of refs in `git for-each-ref`. - Changed "Fetch Refs" to "refs/heads/master". - Ran `bin/repository pull X --trace --verbose`. On deciding to do something: - Before: since "master" did not change, the pull declined to act. - After: the pull detected surplus refs and deleted them. Since the states then matched, it declined further action. On pruning: - Before: if the pull was forced to act, it ran "fetch --prune" with a narrow refspec, which did not prune the working copy. - After: saw working copy pruned explicitly with "update-ref -d" commands. Also, set "Fetch Refs" back to the default (empty) and pulled, saw everything pull. Maniphest Tasks: T13448 Differential Revision: https://secure.phabricator.com/D20892 --- .../PhabricatorRepositoryPullEngine.php | 84 +++++++++++++++---- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index ea70f380aa..a7302341f4 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -353,13 +353,56 @@ final class PhabricatorRepositoryPullEngine // Load the refs we're planning to fetch from the remote repository. $remote_refs = $this->loadGitRemoteRefs( $repository, - $repository->getRemoteURIEnvelope()); + $repository->getRemoteURIEnvelope(), + $is_local = false); // Load the refs we're planning to fetch from the local repository, by // using the local working copy path as the "remote" repository URI. $local_refs = $this->loadGitRemoteRefs( $repository, - new PhutilOpaqueEnvelope($path)); + new PhutilOpaqueEnvelope($path), + $is_local = true); + + // See T13448. The "git fetch --prune ..." flag only prunes local refs + // matching the refspecs we pass it. If "Fetch Refs" is configured, we'll + // pass it a very narrow list of refspecs, and it won't prune older refs + // that aren't currently subject to fetching. + + // Since we want to prune everything that isn't (a) on the fetch list and + // (b) in the remote, handle pruning of any surplus leftover refs ourselves + // before we fetch anything. + + // (We don't have to do this if "Fetch Refs" isn't set up, since "--prune" + // will work in that case, but it's a little simpler to always go down the + // same code path.) + + $surplus_refs = array(); + foreach ($local_refs as $local_ref => $local_hash) { + $remote_hash = idx($remote_refs, $local_ref); + if ($remote_hash === null) { + $surplus_refs[] = $local_ref; + } + } + + if ($surplus_refs) { + $this->log( + pht( + 'Found %s surplus local ref(s) to delete.', + phutil_count($surplus_refs))); + foreach ($surplus_refs as $surplus_ref) { + $this->log( + pht( + 'Deleting surplus local ref "%s" ("%s").', + $surplus_ref, + $local_refs[$surplus_ref])); + + $repository->execLocalCommand( + 'update-ref -d %R --', + $surplus_ref); + + unset($local_refs[$surplus_ref]); + } + } if ($remote_refs === $local_refs) { $this->log( @@ -378,7 +421,7 @@ final class PhabricatorRepositoryPullEngine // checked out. See T13280. $future = $repository->getRemoteCommandFuture( - 'fetch --prune --update-head-ok -- %P %Ls', + 'fetch --update-head-ok -- %P %Ls', $repository->getRemoteURIEnvelope(), $fetch_rules); @@ -474,21 +517,32 @@ final class PhabricatorRepositoryPullEngine private function loadGitRemoteRefs( PhabricatorRepository $repository, - PhutilOpaqueEnvelope $remote_envelope) { + PhutilOpaqueEnvelope $remote_envelope, + $is_local) { - $ref_rules = $this->getGitRefRules($repository); + // See T13448. When listing local remotes, we want to list everything, + // not just refs we expect to fetch. This allows us to detect that we have + // undesirable refs (which have been deleted in the remote, but are still + // present locally) so we can update our state to reflect the correct + // remote state. - // NOTE: "git ls-remote" does not support "--" until circa January 2016. - // See T12416. None of the flags to "ls-remote" appear dangerous, but - // refuse to list any refs beginning with "-" just in case. + if ($is_local) { + $ref_rules = array(); + } else { + $ref_rules = $this->getGitRefRules($repository); - foreach ($ref_rules as $ref_rule) { - if (preg_match('/^-/', $ref_rule)) { - throw new Exception( - pht( - 'Refusing to list potentially dangerous ref ("%s") beginning '. - 'with "-".', - $ref_rule)); + // NOTE: "git ls-remote" does not support "--" until circa January 2016. + // See T12416. None of the flags to "ls-remote" appear dangerous, but + // refuse to list any refs beginning with "-" just in case. + + foreach ($ref_rules as $ref_rule) { + if (preg_match('/^-/', $ref_rule)) { + throw new Exception( + pht( + 'Refusing to list potentially dangerous ref ("%s") beginning '. + 'with "-".', + $ref_rule)); + } } } From 8dd57a1ed221f592579dd925f22979047d289e78 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Nov 2019 15:41:50 -0800 Subject: [PATCH 26/57] When fetching Git repositories, pass "--no-tags" to make explicit "Fetch Refs" operate more narrowly Summary: Ref T13448. The default behavior of "git fetch '+refs/heads/master:refs/heads/master'" is to follow and fetch associated tags. We don't want this when we pass a narrow refspec because of "Fetch Refs" configuration. Tell Git that we only want the refs we explicitly passed. Note that this doesn't prevent us from fetching tags (if the refspec specifies them), it just stops us from fetching extra tags that aren't part of the refspec. Test Plan: - Ran "bin/repository pull X --trace --verbose" in a repository with a "Fetch Refs" configuration, saw only "master" get fetched (previously: "master" and reachable tags). - Ran "git fetch --no-tags '+refs/*:refs/*'", saw tags fetched normally ("--no-tags" does not disable fetching tags). Maniphest Tasks: T13448 Differential Revision: https://secure.phabricator.com/D20893 --- .../repository/engine/PhabricatorRepositoryPullEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index a7302341f4..c228932ac8 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -421,7 +421,7 @@ final class PhabricatorRepositoryPullEngine // checked out. See T13280. $future = $repository->getRemoteCommandFuture( - 'fetch --update-head-ok -- %P %Ls', + 'fetch --no-tags --update-head-ok -- %P %Ls', $repository->getRemoteURIEnvelope(), $fetch_rules); From 1b40f7e5408b7865677f16cb20dcec87a6cd8545 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Nov 2019 16:04:53 -0800 Subject: [PATCH 27/57] Always initialize Git repositories with "git init", never with "git clone" Summary: Fixes T13448. We currently "git clone" to initialize repositories, but this will fetch too many refs if "Fetch Refs" is configured. In modern Phabricator, there's no apparent reason to "git clone"; we can just "git init" instead. This workflow naturally falls through to an update, where we'll do a "git fetch" and pull in exactly the refs we want. Test Plan: - Configured an observed repository with "Fetch Refs". - Destroyed the working copy. - Ran "bin/repository pull X --trace --verbose". - Before: saw "git clone" pull in the world. - After: saw "git init" create a bare empty working copy, then "git fetch" fill it surgically. Both flows end up in the same place, this one is just simpler and does less work. Maniphest Tasks: T13448 Differential Revision: https://secure.phabricator.com/D20894 --- .../PhabricatorRepositoryPullEngine.php | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index c228932ac8..2b008b630b 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -257,16 +257,15 @@ final class PhabricatorRepositoryPullEngine $path = rtrim($repository->getLocalPath(), '/'); - if ($repository->isHosted()) { - $repository->execxRemoteCommand( - 'init --bare -- %s', - $path); - } else { - $repository->execxRemoteCommand( - 'clone --bare -- %P %s', - $repository->getRemoteURIEnvelope(), - $path); - } + // See T13448. In all cases, we create repositories by using "git init" + // to build a bare, empty working copy. If we try to use "git clone" + // instead, we'll pull in too many refs if "Fetch Refs" is also + // configured. There's no apparent way to make "git clone" behave narrowly + // and no apparent reason to bother. + + $repository->execxRemoteCommand( + 'init --bare -- %s', + $path); } @@ -290,29 +289,27 @@ final class PhabricatorRepositoryPullEngine $files = Filesystem::listDirectory($path, $include_hidden = true); if (!$files) { $message = pht( - "Expected to find a git repository at '%s', but there ". - "is an empty directory there. Remove the directory: the daemon ". - "will run '%s' for you.", - $path, - 'git clone'); + 'Expected to find a Git repository at "%s", but there is an '. + 'empty directory there. Remove the directory. A daemon will '. + 'construct the working copy for you.', + $path); } else { $message = pht( - "Expected to find a git repository at '%s', but there is ". - "a non-repository directory (with other stuff in it) there. Move ". - "or remove this directory (or reconfigure the repository to use a ". - "different directory), and then either clone a repository ". - "yourself or let the daemon do it.", + 'Expected to find a Git repository at "%s", but there is '. + 'a non-repository directory (with other stuff in it) there. '. + 'Move or remove this directory. A daemon will construct '. + 'the working copy for you.', $path); } } else if (is_file($path)) { $message = pht( - "Expected to find a git repository at '%s', but there is a ". - "file there instead. Remove it and let the daemon clone a ". - "repository for you.", + 'Expected to find a Git repository at "%s", but there is a '. + 'file there instead. Move or remove this file. A daemon will '. + 'construct the working copy for you.', $path); } else { $message = pht( - "Expected to find a git repository at '%s', but did not.", + 'Expected to find a git repository at "%s", but did not.', $path); } } else { @@ -327,11 +324,10 @@ final class PhabricatorRepositoryPullEngine } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { $err = true; $message = pht( - "Expected to find repo at '%s', but the actual git repository root ". - "for this directory is '%s'. Something is misconfigured. ". - "The repository's 'Local Path' should be set to some place where ". - "the daemon can check out a working copy, ". - "and should not be inside another git repository.", + 'Expected to find a Git repository at "%s", but the actual Git '. + 'repository root for this directory is "%s". Something is '. + 'misconfigured. This directory should be writable by the daemons '. + 'and not inside another Git repository.', $path, $repo_path); } From 502ca4767e4b4ce5d06d83e2889c3696e2df22ef Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Nov 2019 16:24:49 -0800 Subject: [PATCH 28/57] When "Fetch Refs" are configured for a repository, highlight the "Branches" menu item in the Diffusion Management UI Summary: Ref T13448. Minor UI issue: setting a "Fetch Refs" value does not highlight the associated menu item, but should. Test Plan: Set only "Fetch Refs", now saw menu item highlighted. Maniphest Tasks: T13448 Differential Revision: https://secure.phabricator.com/D20895 --- .../management/DiffusionRepositoryBranchesManagementPanel.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php index 43b4d31252..e24bae7da4 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBranchesManagementPanel.php @@ -23,6 +23,7 @@ final class DiffusionRepositoryBranchesManagementPanel $has_any = $repository->getDetail('default-branch') || + $repository->getFetchRules() || $repository->getTrackOnlyRules() || $repository->getPermanentRefRules(); From d4491ddc225e8f02015b6f475d842d58e9b55c50 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Nov 2019 07:33:16 -0800 Subject: [PATCH 29/57] Fix an issue with 1up diff block rendering for added or removed blocks Summary: Ref T13425. When a change adds or removes a block (vs adding or removing a document, or changing a block), we currently try to render `null` as cell content in the unified view. Make this check broader to catch both "no opposite document" and "no opposite cell". Test Plan: {F7008772} Subscribers: artms Maniphest Tasks: T13425 Differential Revision: https://secure.phabricator.com/D20897 --- .../render/DifferentialChangesetOneUpRenderer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index 19c939274d..b3e6520fd9 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -371,7 +371,7 @@ final class DifferentialChangesetOneUpRenderer $cell_classes = $block_diff->getNewClasses(); } } else if ($row_type === 'old') { - if (!$old_ref) { + if (!$old_ref || !$old) { continue; } @@ -384,7 +384,7 @@ final class DifferentialChangesetOneUpRenderer $new_key = null; } else if ($row_type === 'new') { - if (!$new_ref) { + if (!$new_ref || !$new) { continue; } From 338b4cb2e70950c1cd4795d0dbb79eca9567f18d Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Nov 2019 08:17:45 -0800 Subject: [PATCH 30/57] Prevent workboard cards from being grabbed by the "Txxx" object name text Summary: Fixes T13452. We currently give users mixed signals about the interaction mode of this text: the cursor says "text" but the behavior is "grab". Make the behavior "text" to align with the cursor. An alternate variation of this change is to remove the cursor, but this is preferable if it doesn't cause problems, since copying the task ID is at least somewhat useful. Test Plan: In Safari, Firefox, and Chrome: selected and copied object names from workboard cards; and dragged workboard cards by other parts of their UI. Maniphest Tasks: T13452 Differential Revision: https://secure.phabricator.com/D20898 --- resources/celerity/map.php | 22 +++++++++++----------- src/view/phui/PHUIObjectItemView.php | 3 ++- webroot/rsrc/js/core/DraggableList.js | 9 +++++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8547f8a0a3..ffe4024ed7 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '77de226f', - 'core.pkg.js' => '6e5c894f', + 'core.pkg.js' => '705aec2c', 'differential.pkg.css' => '607c84be', 'differential.pkg.js' => '1b97518d', 'diffusion.pkg.css' => '42c75c37', @@ -448,7 +448,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => 'c9ad6f70', + 'rsrc/js/core/DraggableList.js' => '0169e425', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -777,7 +777,7 @@ return array( 'phabricator-diff-changeset-list' => '0f5c016d', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => 'c9ad6f70', + 'phabricator-draggable-list' => '0169e425', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -920,6 +920,14 @@ return array( 'javelin-uri', 'phabricator-notification', ), + '0169e425' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '022516b4' => array( 'javelin-install', 'javelin-util', @@ -2032,14 +2040,6 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), - 'c9ad6f70' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 05747c7ce6..b5ad5a7fd6 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -379,10 +379,11 @@ final class PHUIObjectItemView extends AphrontTagView { if ($this->objectName) { $header_name[] = array( - phutil_tag( + javelin_tag( 'span', array( 'class' => 'phui-oi-objname', + 'sigil' => 'ungrabbable', ), $this->objectName), ' ', diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 5f19b7061d..8930f43f94 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -181,6 +181,15 @@ JX.install('DraggableList', { return; } + // See T13452. If this is an ungrabble part of the item, don't start a + // drag. We use this to allow users to select text on cards. + var target = e.getTarget(); + if (target) { + if (JX.Stratcom.hasSigil(target, 'ungrabbable')) { + return; + } + } + if (JX.Stratcom.pass()) { // Let other handlers deal with this event before we do. return; From 2223d6b914678b402e2049bf321492a7ca8e9d59 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Nov 2019 08:46:37 -0800 Subject: [PATCH 31/57] Update Asana Auth adapter for "gid" API changes Summary: Ref T13453. The Asana API has changed, replacing all "id" fields with "gid", including the "users/me" API call result. Test Plan: Linked an Asana account. Before: error about missing 'id'. After: clean link. Maniphest Tasks: T13453 Differential Revision: https://secure.phabricator.com/D20899 --- src/applications/auth/adapter/PhutilAsanaAuthAdapter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php b/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php index 5d9a9ec478..5fe343671e 100644 --- a/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php +++ b/src/applications/auth/adapter/PhutilAsanaAuthAdapter.php @@ -14,7 +14,9 @@ final class PhutilAsanaAuthAdapter extends PhutilOAuthAuthAdapter { } public function getAccountID() { - return $this->getOAuthAccountData('id'); + // See T13453. The Asana API has changed to string IDs and now returns a + // "gid" field (previously, it returned an "id" field). + return $this->getOAuthAccountData('gid'); } public function getAccountEmail() { From cd60a8aa563bfc733a0bccde451bc3974a688418 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Nov 2019 08:57:35 -0800 Subject: [PATCH 32/57] Update various Asana odds-and-ends for "gid" API changes Summary: Ref T13453. Some of the Asana integrations also need API updates. Depends on D20899. Test Plan: - Viewed "asana.workspace-id" in Config, got a sensible GID list. - Created a revision, saw the associated Asana task get assigned. - Pasted an Asana link I could view into a revision description, saw it Doorkeeper in the metadata. Maniphest Tasks: T13453 Differential Revision: https://secure.phabricator.com/D20900 --- .../doorkeeper/bridge/DoorkeeperBridgeAsana.php | 7 +++++-- .../doorkeeper/option/PhabricatorAsanaConfigOptions.php | 5 ++++- .../doorkeeper/worker/DoorkeeperAsanaFeedWorker.php | 8 ++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php index 05ee786337..59c3e4026d 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php @@ -123,8 +123,11 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge { } public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { - $id = $result['id']; - $uri = "https://app.asana.com/0/{$id}/{$id}"; + $gid = $result['gid']; + $uri = urisprintf( + 'https://app.asana.com/0/%s/%s', + $gid, + $gid); $obj->setObjectURI($uri); } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index 1771a6615e..3a9c9abac5 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -102,7 +102,10 @@ final class PhabricatorAsanaConfigOptions pht('Workspace Name')); $out[] = '| ------------ | -------------- |'; foreach ($workspaces as $workspace) { - $out[] = sprintf('| `%s` | `%s` |', $workspace['id'], $workspace['name']); + $out[] = sprintf( + '| `%s` | `%s` |', + $workspace['gid'], + $workspace['name']); } $out = implode("\n", $out); diff --git a/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php b/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php index 1d293956b3..00b75b7a56 100644 --- a/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php +++ b/src/applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php @@ -358,7 +358,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { 'POST', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], - 'completed' => $is_completed, + 'completed' => (int)$is_completed, 'parent' => $parent_ref->getObjectID(), )); @@ -393,7 +393,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { 'PUT', $subtask_data + array( 'assignee' => $phid_aid_map[$user_phid], - 'completed' => $is_completed, + 'completed' => (int)$is_completed, )); } @@ -484,7 +484,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { return array( 'name' => $title, 'notes' => $notes, - 'completed' => $is_completed, + 'completed' => (int)$is_completed, ); } @@ -632,7 +632,7 @@ final class DoorkeeperAsanaFeedWorker extends DoorkeeperFeedWorker { ->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA) ->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA) ->setObjectType($type) - ->setObjectID($result['id']) + ->setObjectID($result['gid']) ->setIsVisible(true); $xobj = $ref->newExternalObject(); From b83b3224bb756a2c2e29727df01b1c419f2c832b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Nov 2019 10:13:13 -0700 Subject: [PATCH 33/57] Add an "Advanced/Developer..." action item for viewing object handle details and hovercards Summary: Ref T13442. Ref T13157. There's a secret URI to look at an object's hovercard in a standalone view, but it's hard to remember and impossible to discover. In developer mode, add an action to "View Hovercard". Also add "View Handle", which primarily shows the object PHID. Test Plan: Viewed some objects, saw "Advanced/Developer...". Used "View Hovercard" to view hovercards and "View Handle" to view handles. Maniphest Tasks: T13442, T13157 Differential Revision: https://secure.phabricator.com/D20887 --- src/__phutil_library_map__.php | 4 + .../PhabricatorSearchApplication.php | 2 + .../PhabricatorSearchHandleController.php | 89 +++++++++++++++++++ .../PhabricatorSystemApplication.php | 6 ++ .../PhabricatorSystemDebugUIEventListener.php | 58 ++++++++++++ src/view/layout/PhabricatorActionListView.php | 8 ++ 6 files changed, 167 insertions(+) create mode 100644 src/applications/search/controller/PhabricatorSearchHandleController.php create mode 100644 src/applications/system/events/PhabricatorSystemDebugUIEventListener.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2db41f0a69..bb6aeb20a1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4648,6 +4648,7 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineExtensionModule' => 'applications/search/engineextension/PhabricatorSearchEngineExtensionModule.php', 'PhabricatorSearchFerretNgramGarbageCollector' => 'applications/search/garbagecollector/PhabricatorSearchFerretNgramGarbageCollector.php', 'PhabricatorSearchField' => 'applications/search/field/PhabricatorSearchField.php', + 'PhabricatorSearchHandleController' => 'applications/search/controller/PhabricatorSearchHandleController.php', 'PhabricatorSearchHost' => 'infrastructure/cluster/search/PhabricatorSearchHost.php', 'PhabricatorSearchHovercardController' => 'applications/search/controller/PhabricatorSearchHovercardController.php', 'PhabricatorSearchIndexVersion' => 'applications/search/storage/PhabricatorSearchIndexVersion.php', @@ -4872,6 +4873,7 @@ phutil_register_library_map(array( 'PhabricatorSystemActionRateLimitException' => 'applications/system/exception/PhabricatorSystemActionRateLimitException.php', 'PhabricatorSystemApplication' => 'applications/system/application/PhabricatorSystemApplication.php', 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', + 'PhabricatorSystemDebugUIEventListener' => 'applications/system/events/PhabricatorSystemDebugUIEventListener.php', 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', 'PhabricatorSystemObjectController' => 'applications/system/controller/PhabricatorSystemObjectController.php', @@ -11270,6 +11272,7 @@ phutil_register_library_map(array( 'PhabricatorSearchEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorSearchFerretNgramGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSearchField' => 'Phobject', + 'PhabricatorSearchHandleController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchHost' => 'Phobject', 'PhabricatorSearchHovercardController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchIndexVersion' => 'PhabricatorSearchDAO', @@ -11511,6 +11514,7 @@ phutil_register_library_map(array( 'PhabricatorSystemActionRateLimitException' => 'Exception', 'PhabricatorSystemApplication' => 'PhabricatorApplication', 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', + 'PhabricatorSystemDebugUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', 'PhabricatorSystemObjectController' => 'PhabricatorController', diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php index 3cf5923b9c..7547506258 100644 --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -33,6 +33,8 @@ final class PhabricatorSearchApplication extends PhabricatorApplication { 'index/(?P[^/]+)/' => 'PhabricatorSearchIndexController', 'hovercard/' => 'PhabricatorSearchHovercardController', + 'handle/(?P[^/]+)/' + => 'PhabricatorSearchHandleController', 'edit/' => array( 'key/(?P[^/]+)/' => 'PhabricatorSearchEditController', 'id/(?P[^/]+)/' => 'PhabricatorSearchEditController', diff --git a/src/applications/search/controller/PhabricatorSearchHandleController.php b/src/applications/search/controller/PhabricatorSearchHandleController.php new file mode 100644 index 0000000000..751b4e367d --- /dev/null +++ b/src/applications/search/controller/PhabricatorSearchHandleController.php @@ -0,0 +1,89 @@ +getViewer(); + $phid = $request->getURIData('phid'); + + $handles = $viewer->loadHandles(array($phid)); + $handle = $handles[$phid]; + + $cancel_uri = $handle->getURI(); + if (!$cancel_uri) { + $cancel_uri = '/'; + } + + $rows = array(); + + $rows[] = array( + pht('PHID'), + $phid, + ); + + $rows[] = array( + pht('PHID Type'), + phid_get_type($phid), + ); + + $rows[] = array( + pht('URI'), + $handle->getURI(), + ); + + $icon = $handle->getIcon(); + if ($icon !== null) { + $icon = id(new PHUIIconView()) + ->setIcon($handle->getIcon()); + } + + $rows[] = array( + pht('Icon'), + $icon, + ); + + $rows[] = array( + pht('Object Name'), + $handle->getObjectName(), + ); + + $rows[] = array( + pht('Name'), + $handle->getName(), + ); + + $rows[] = array( + pht('Full Name'), + $handle->getFullName(), + ); + + $rows[] = array( + pht('Tag'), + $handle->renderTag(), + ); + + $rows[] = array( + pht('Link'), + $handle->renderLink(), + ); + + $table = id(new AphrontTableView($rows)) + ->setColumnClasses( + array( + 'header', + 'wide', + )); + + return $this->newDialog() + ->setTitle(pht('Handle: %s', $phid)) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($table) + ->addCancelButton($cancel_uri, pht('Done')); + } + +} diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php index b6cc13050f..88f07ae17c 100644 --- a/src/applications/system/application/PhabricatorSystemApplication.php +++ b/src/applications/system/application/PhabricatorSystemApplication.php @@ -14,6 +14,12 @@ final class PhabricatorSystemApplication extends PhabricatorApplication { return true; } + public function getEventListeners() { + return array( + new PhabricatorSystemDebugUIEventListener(), + ); + } + public function getRoutes() { return array( '/status/' => 'PhabricatorStatusController', diff --git a/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php b/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php new file mode 100644 index 0000000000..18b94323b6 --- /dev/null +++ b/src/applications/system/events/PhabricatorSystemDebugUIEventListener.php @@ -0,0 +1,58 @@ +listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); + } + + public function handleEvent(PhutilEvent $event) { + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: + $this->handleActionEvent($event); + break; + } + } + + private function handleActionEvent($event) { + $viewer = $event->getUser(); + $object = $event->getValue('object'); + + if (!PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { + return; + } + + if (!$object || !$object->getPHID()) { + // If we have no object, or the object doesn't have a PHID, we can't + // do anything useful. + return; + } + + $phid = $object->getPHID(); + + $submenu = array(); + + $submenu[] = id(new PhabricatorActionView()) + ->setIcon('fa-asterisk') + ->setName(pht('View Handle')) + ->setHref(urisprintf('/search/handle/%s/', $phid)) + ->setWorkflow(true); + + $submenu[] = id(new PhabricatorActionView()) + ->setIcon('fa-address-card-o') + ->setName(pht('View Hovercard')) + ->setHref(urisprintf('/search/hovercard/?phids[]=%s', $phid)); + + $developer_action = id(new PhabricatorActionView()) + ->setName(pht('Advanced/Developer...')) + ->setIcon('fa-magic') + ->setOrder(9001) + ->setSubmenu($submenu); + + $actions = $event->getValue('actions'); + $actions[] = $developer_action; + $event->setValue('actions', $actions); + } + +} diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php index 134c336735..22e995ab64 100644 --- a/src/view/layout/PhabricatorActionListView.php +++ b/src/view/layout/PhabricatorActionListView.php @@ -52,6 +52,14 @@ final class PhabricatorActionListView extends AphrontTagView { $action->setViewer($viewer); } + $sort = array(); + foreach ($actions as $key => $action) { + $sort[$key] = id(new PhutilSortVector()) + ->addInt($action->getOrder()); + } + $sort = msortv($sort, 'getSelf'); + $actions = array_select_keys($actions, array_keys($sort)); + require_celerity_resource('phabricator-action-list-view-css'); $items = array(); From a3f4cbd7484b591425e83bbfc7642bccb04d0d57 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Nov 2019 16:53:11 -0800 Subject: [PATCH 34/57] Correct rendering of workboard column move stories when a single transaction performs moves on multiple boards Summary: See . If a single transaction performs column moves on multiple different boards (which is permitted in the API), the rendering logic currently fails. Make it render properly. Test Plan: {F7011464} Differential Revision: https://secure.phabricator.com/D20901 --- .../transactions/storage/PhabricatorApplicationTransaction.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 7e65ac0a09..1ec29557da 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1133,6 +1133,8 @@ abstract class PhabricatorApplicationTransaction } else { $fragments = array(); foreach ($moves as $move) { + $to_column = $move['columnPHID']; + $board_phid = $move['boardPHID']; $fragments[] = pht( '%s (%s)', $this->renderHandleLink($board_phid), From 72f82abe0723a866856fa871f24a5e40ad9642bc Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Nov 2019 17:08:44 -0800 Subject: [PATCH 35/57] Improve recovery from panel action rendering exceptions, and mark "Changeset" queries as not suitable for panel generation Summary: Fixes T13443. When a panel raises an exception during edit action generation, it currently escapes to top level. Instead, catch it more narrowly. Additionally, mark "ChangesetSearchEngine" as not being a suitable search engine for use in query panels. There's no list view or search URI so it can't generate a sensible panel. Test Plan: - Added a changeset panel to a dashboard. - Before: entire dashboard fataled. - After: panel fataled narrowly, menu fatals narrowly, dashboard no longer permits creation of another Changeset query panel. Maniphest Tasks: T13443 Differential Revision: https://secure.phabricator.com/D20902 --- .../PhabricatorDashboardPanelRenderingEngine.php | 13 ++++++++++--- .../query/DifferentialChangesetSearchEngine.php | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index 29a710209e..8969151c06 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -329,9 +329,16 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { $actions = array(); if ($panel) { - $panel_actions = $panel->newHeaderEditActions( - $viewer, - $context_phid); + try { + $panel_actions = $panel->newHeaderEditActions( + $viewer, + $context_phid); + } catch (Exception $ex) { + $error_action = id(new PhabricatorActionView()) + ->setIcon('fa-exclamation-triangle red') + ->setName(pht('')); + $panel_actions[] = $error_action; + } if ($panel_actions) { foreach ($panel_actions as $panel_action) { diff --git a/src/applications/differential/query/DifferentialChangesetSearchEngine.php b/src/applications/differential/query/DifferentialChangesetSearchEngine.php index 0dfec94a53..3fe8957971 100644 --- a/src/applications/differential/query/DifferentialChangesetSearchEngine.php +++ b/src/applications/differential/query/DifferentialChangesetSearchEngine.php @@ -22,6 +22,10 @@ final class DifferentialChangesetSearchEngine return 'PhabricatorDifferentialApplication'; } + public function canUseInPanelContext() { + return false; + } + public function newQuery() { $query = id(new DifferentialChangesetQuery()); From 2adc36ba0b8dfd9d801ee37b3079f6a42dca4429 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 9 Nov 2019 08:32:24 -0800 Subject: [PATCH 36/57] Correctly identify more SSH private key problems as "formatting" or "passphrase" related Summary: Ref T13454. Fixes T13006. When a user provide us with an SSH private key and (possibly) a passphrase: # Try to verify that they're correct by extracting the public key. # If that fails, try to figure out why it didn't work. Our success in step (2) will vary depending on what the problem is, and we may end up falling through to a very generic error, but the outcome should generally be better than the old approach. Previously, we had a very unsophisticated test for the text "ENCRYPTED" in the key body and questionable handling of the results: for example, providing a passphrase when a key did not require one did not raise an error. Test Plan: Created and edited credentials with: - Valid, passphrase-free keys. - Valid, passphrased keys with the right passphrase. - Valid, passphrase-free keys with a passphrase ("surplus passphrase" error). - Valid, passphrased keys with no passphrase ("missing passphrase" error). - Valid, passphrased keys with an invalid passphrase ("invalid passphrase" error). - Invalid keys ("format" error). The precision of these errors will vary depending on how helpful "ssh-keygen" is. Maniphest Tasks: T13454, T13006 Differential Revision: https://secure.phabricator.com/D20905 --- src/__phutil_library_map__.php | 16 ++ .../PhabricatorAuthSSHPrivateKeyException.php | 9 + ...icatorAuthSSHPrivateKeyFormatException.php | 14 ++ ...PrivateKeyIncorrectPassphraseException.php | 4 + ...SHPrivateKeyMissingPassphraseException.php | 4 + ...orAuthSSHPrivateKeyPassphraseException.php | 14 ++ ...SHPrivateKeySurplusPassphraseException.php | 4 + ...catorAuthSSHPrivateKeyUnknownException.php | 14 ++ .../sshkey/PhabricatorAuthSSHPrivateKey.php | 210 ++++++++++++++++++ .../PassphraseCredentialEditController.php | 50 +++-- .../PassphraseCredentialType.php | 29 --- ...sphraseSSHPrivateKeyTextCredentialType.php | 36 --- 12 files changed, 323 insertions(+), 81 deletions(-) create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyFormatException.php create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException.php create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyMissingPassphraseException.php create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyPassphraseException.php create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeySurplusPassphraseException.php create mode 100644 src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyUnknownException.php create mode 100644 src/applications/auth/sshkey/PhabricatorAuthSSHPrivateKey.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bb6aeb20a1..0927f450b2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2439,6 +2439,14 @@ phutil_register_library_map(array( 'PhabricatorAuthSSHKeyTransaction' => 'applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php', 'PhabricatorAuthSSHKeyTransactionQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyTransactionQuery.php', 'PhabricatorAuthSSHKeyViewController' => 'applications/auth/controller/PhabricatorAuthSSHKeyViewController.php', + 'PhabricatorAuthSSHPrivateKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPrivateKey.php', + 'PhabricatorAuthSSHPrivateKeyException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php', + 'PhabricatorAuthSSHPrivateKeyFormatException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyFormatException.php', + 'PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeyMissingPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyMissingPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeyPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeySurplusPassphraseException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeySurplusPassphraseException.php', + 'PhabricatorAuthSSHPrivateKeyUnknownException' => 'applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyUnknownException.php', 'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php', 'PhabricatorAuthSSHRevoker' => 'applications/auth/revoker/PhabricatorAuthSSHRevoker.php', 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', @@ -8679,6 +8687,14 @@ phutil_register_library_map(array( 'PhabricatorAuthSSHKeyTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthSSHKeyTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthSSHKeyViewController' => 'PhabricatorAuthSSHKeyController', + 'PhabricatorAuthSSHPrivateKey' => 'Phobject', + 'PhabricatorAuthSSHPrivateKeyException' => 'Exception', + 'PhabricatorAuthSSHPrivateKeyFormatException' => 'PhabricatorAuthSSHPrivateKeyException', + 'PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException' => 'PhabricatorAuthSSHPrivateKeyPassphraseException', + 'PhabricatorAuthSSHPrivateKeyMissingPassphraseException' => 'PhabricatorAuthSSHPrivateKeyPassphraseException', + 'PhabricatorAuthSSHPrivateKeyPassphraseException' => 'PhabricatorAuthSSHPrivateKeyException', + 'PhabricatorAuthSSHPrivateKeySurplusPassphraseException' => 'PhabricatorAuthSSHPrivateKeyPassphraseException', + 'PhabricatorAuthSSHPrivateKeyUnknownException' => 'PhabricatorAuthSSHPrivateKeyException', 'PhabricatorAuthSSHPublicKey' => 'Phobject', 'PhabricatorAuthSSHRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthSession' => array( diff --git a/src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php b/src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php new file mode 100644 index 0000000000..a86e7dafaa --- /dev/null +++ b/src/applications/auth/exception/privatekey/PhabricatorAuthSSHPrivateKeyException.php @@ -0,0 +1,9 @@ + + } + + public function setPassphrase(PhutilOpaqueEnvelope $passphrase) { + $this->passphrase = $passphrase; + return $this; + } + + public function getPassphrase() { + return $this->passphrase; + } + + public static function newFromRawKey(PhutilOpaqueEnvelope $entire_key) { + $key = new self(); + + $key->body = $entire_key; + + return $key; + } + + public function getKeyBody() { + return $this->body; + } + + public function newBarePrivateKey() { + if (!Filesystem::binaryExists('ssh-keygen')) { + throw new Exception( + pht( + 'Analyzing or decrypting SSH keys requires the "ssh-keygen" binary, '. + 'but it is not available in "$PATH". Make it available to work with '. + 'SSH private keys.')); + } + + $old_body = $this->body; + + // Some versions of "ssh-keygen" are sensitive to trailing whitespace for + // some keys. Trim any trailing whitespace and replace it with a single + // newline. + $raw_body = $old_body->openEnvelope(); + $raw_body = rtrim($raw_body)."\n"; + $old_body = new PhutilOpaqueEnvelope($raw_body); + + $tmp = $this->newTemporaryPrivateKeyFile($old_body); + + // See T13454 for discussion of why this is so awkward. In broad strokes, + // we don't have a straightforward way to distinguish between keys with an + // invalid format and keys with a passphrase which we don't know. + + // First, try to extract the public key from the file using the (possibly + // empty) passphrase we were given. If everything is in good shape, this + // should work. + + $passphrase = $this->getPassphrase(); + if ($passphrase) { + list($err, $stdout, $stderr) = exec_manual( + 'ssh-keygen -y -P %P -f %R', + $passphrase, + $tmp); + } else { + list($err, $stdout, $stderr) = exec_manual( + 'ssh-keygen -y -P %s -f %R', + '', + $tmp); + } + + // If that worked, the key is good and the (possibly empty) passphrase is + // correct. Strip the passphrase if we have one, then return the bare key. + + if (!$err) { + if ($passphrase) { + execx( + 'ssh-keygen -y -P %P -N %s -f %R', + $passphrase, + '', + $tmp); + + $new_body = new PhutilOpaqueEnvelope(Filesystem::readFile($tmp)); + unset($tmp); + } else { + $new_body = $old_body; + } + + return self::newFromRawKey($new_body); + } + + // We were not able to extract the public key. Try to figure out why. The + // reasons we expect are: + // + // - We were given a passphrase, but the key has no passphrase. + // - We were given a passphrase, but the passphrase is wrong. + // - We were not given a passphrase, but the key has a passphrase. + // - The key format is invalid. + // + // Our ability to separate these cases varies a lot, particularly because + // some versions of "ssh-keygen" return very similar diagnostic messages + // for any error condition. Try our best. + + if ($passphrase) { + // First, test for "we were given a passphrase, but the key has no + // passphrase", since this is a conclusive test. + list($err) = exec_manual( + 'ssh-keygen -y -P %s -f %R', + '', + $tmp); + if (!$err) { + throw new PhabricatorAuthSSHPrivateKeySurplusPassphraseException( + pht( + 'A passphrase was provided for this private key, but it does '. + 'not require a passphrase. Check that you supplied the correct '. + 'key, or omit the passphrase.')); + } + } + + // We're out of conclusive tests, so try to guess why the error occurred. + // In some versions of "ssh-keygen", we get a usable diagnostic message. In + // other versions, not so much. + + $reason_format = 'format'; + $reason_passphrase = 'passphrase'; + $reason_unknown = 'unknown'; + + $patterns = array( + // macOS 10.14.6 + '/incorrect passphrase supplied to decrypt private key/' + => $reason_passphrase, + + // macOS 10.14.6 + '/invalid format/' => $reason_format, + + // Ubuntu 14 + '/load failed/' => $reason_unknown, + ); + + $reason = 'unknown'; + foreach ($patterns as $pattern => $pattern_reason) { + $ok = preg_match($pattern, $stderr); + + if ($ok === false) { + throw new Exception( + pht( + 'Pattern "%s" is not valid.', + $pattern)); + } + + if ($ok) { + $reason = $pattern_reason; + break; + } + } + + if ($reason === $reason_format) { + throw new PhabricatorAuthSSHPrivateKeyFormatException( + pht( + 'This private key is not formatted correctly. Check that you '. + 'have provided the complete text of a valid private key.')); + } + + if ($reason === $reason_passphrase) { + if ($passphrase) { + throw new PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException( + pht( + 'This private key requires a passphrase, but the wrong '. + 'passphrase was provided. Check that you supplied the correct '. + 'key and passphrase.')); + } else { + throw new PhabricatorAuthSSHPrivateKeyIncorrectPassphraseException( + pht( + 'This private key requires a passphrase, but no passphrase was '. + 'provided. Check that you supplied the correct key, or provide '. + 'the passphrase.')); + } + } + + if ($passphrase) { + throw new PhabricatorAuthSSHPrivateKeyUnknownException( + pht( + 'This private key could not be opened with the provided passphrase. '. + 'This might mean that the passphrase is wrong or that the key is '. + 'not formatted correctly. Check that you have supplied the '. + 'complete text of a valid private key and the correct passphrase.')); + } else { + throw new PhabricatorAuthSSHPrivateKeyUnknownException( + pht( + 'This private key could not be opened. This might mean that the '. + 'key requires a passphrase, or might mean that the key is not '. + 'formatted correctly. Check that you have supplied the complete '. + 'text of a valid private key and the correct passphrase.')); + } + } + + private function newTemporaryPrivateKeyFile(PhutilOpaqueEnvelope $key_body) { + $tmp = new TempFile(); + + Filesystem::writeFile($tmp, $key_body->openEnvelope()); + + return $tmp; + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 91c8d93883..a3a346c20f 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -80,6 +80,7 @@ final class PassphraseCredentialEditController extends PassphraseController { $validation_exception = null; $errors = array(); $e_password = null; + $e_secret = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); @@ -97,22 +98,36 @@ final class PassphraseCredentialEditController extends PassphraseController { $env_secret = new PhutilOpaqueEnvelope($v_secret); $env_password = new PhutilOpaqueEnvelope($v_password); - if ($type->requiresPassword($env_secret)) { + $has_secret = !preg_match('/^('.$bullet.')+$/', trim($v_decrypt)); + + // Validate and repair SSH private keys, and apply passwords if they + // are provided. See T13454 for discussion. + + // This should eventually be refactored to be modular rather than a + // hard-coded set of behaviors here in the Controller, but this is + // likely a fairly extensive change. + + $is_ssh = ($type instanceof PassphraseSSHPrivateKeyTextCredentialType); + + if ($is_ssh && $has_secret) { + $old_object = PhabricatorAuthSSHPrivateKey::newFromRawKey($env_secret); + if (strlen($v_password)) { - $v_decrypt = $type->decryptSecret($env_secret, $env_password); - if ($v_decrypt === null) { - $e_password = pht('Incorrect'); - $errors[] = pht( - 'This key requires a password, but the password you provided '. - 'is incorrect.'); - } else { - $v_decrypt = $v_decrypt->openEnvelope(); + $old_object->setPassphrase($env_password); + } + + try { + $new_object = $old_object->newBarePrivateKey(); + $v_decrypt = $new_object->getKeyBody()->openEnvelope(); + } catch (PhabricatorAuthSSHPrivateKeyException $ex) { + $errors[] = $ex->getMessage(); + + if ($ex->isFormatException()) { + $e_secret = pht('Invalid'); + } + if ($ex->isPassphraseException()) { + $e_password = pht('Invalid'); } - } else { - $e_password = pht('Required'); - $errors[] = pht( - 'This key requires a password. You must provide the password '. - 'for the key.'); } } @@ -166,13 +181,14 @@ final class PassphraseCredentialEditController extends PassphraseController { ->setTransactionType($type_username) ->setNewValue($v_username); } + // If some value other than a sequence of bullets was provided for // the credential, update it. In particular, note that we are // explicitly allowing empty secrets: one use case is HTTP auth where // the username is a secret token which covers both identity and // authentication. - if (!preg_match('/^('.$bullet.')+$/', trim($v_decrypt))) { + if ($has_secret) { // If the credential was previously destroyed, restore it when it is // edited if a secret is provided. $xactions[] = id(new PassphraseCredentialTransaction()) @@ -182,6 +198,7 @@ final class PassphraseCredentialEditController extends PassphraseController { $new_secret = id(new PassphraseSecret()) ->setSecretData($v_decrypt) ->save(); + $xactions[] = id(new PassphraseCredentialTransaction()) ->setTransactionType($type_secret_id) ->setNewValue($new_secret->getID()); @@ -287,7 +304,8 @@ final class PassphraseCredentialEditController extends PassphraseController { ->setName('secret') ->setLabel($type->getSecretLabel()) ->setDisabled($credential_is_locked) - ->setValue($v_secret)); + ->setValue($v_secret) + ->setError($e_secret)); if ($type->shouldShowPasswordField()) { $form->appendChild( diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php index 60cb9bb5ae..58c09fac00 100644 --- a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php @@ -102,35 +102,6 @@ abstract class PassphraseCredentialType extends Phobject { return pht('Password'); } - - /** - * Return true if the provided credential requires a password to decrypt. - * - * @param PhutilOpaqueEnvelope Credential secret value. - * @return bool True if the credential needs a password. - * - * @task password - */ - public function requiresPassword(PhutilOpaqueEnvelope $secret) { - return false; - } - - - /** - * Return the decrypted credential secret, or `null` if the password does - * not decrypt the credential. - * - * @param PhutilOpaqueEnvelope Credential secret value. - * @param PhutilOpaqueEnvelope Credential password. - * @return - * @task password - */ - public function decryptSecret( - PhutilOpaqueEnvelope $secret, - PhutilOpaqueEnvelope $password) { - return $secret; - } - public function shouldRequireUsername() { return true; } diff --git a/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php index fa686aac4c..1abe3b351d 100644 --- a/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php +++ b/src/applications/passphrase/credentialtype/PassphraseSSHPrivateKeyTextCredentialType.php @@ -29,40 +29,4 @@ final class PassphraseSSHPrivateKeyTextCredentialType return pht('Password for Key'); } - public function requiresPassword(PhutilOpaqueEnvelope $secret) { - // According to the internet, this is the canonical test for an SSH private - // key with a password. - return preg_match('/ENCRYPTED/', $secret->openEnvelope()); - } - - public function decryptSecret( - PhutilOpaqueEnvelope $secret, - PhutilOpaqueEnvelope $password) { - - $tmp = new TempFile(); - Filesystem::writeFile($tmp, $secret->openEnvelope()); - - if (!Filesystem::binaryExists('ssh-keygen')) { - throw new Exception( - pht( - 'Decrypting SSH keys requires the `%s` binary, but it '. - 'is not available in %s. Either make it available or strip the '. - 'password from this SSH key manually before uploading it.', - 'ssh-keygen', - '$PATH')); - } - - list($err, $stdout, $stderr) = exec_manual( - 'ssh-keygen -p -P %P -N %s -f %s', - $password, - '', - (string)$tmp); - - if ($err) { - return null; - } else { - return new PhutilOpaqueEnvelope(Filesystem::readFile($tmp)); - } - } - } From e86aae99de0b4399b98e190b79a30c7fc1430f76 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Nov 2019 10:29:08 -0800 Subject: [PATCH 37/57] Surface edits to "Text" panels on dashboards as remarkup edits Summary: Fixes T13456. These edits are remarkup edits and should attach files, trigger mentions, and so on. Test Plan: Created a text panel, dropped a file in. After changes, saw the file attach properly. Maniphest Tasks: T13456 Differential Revision: https://secure.phabricator.com/D20906 --- .../PhabricatorDashboardTextPanelTextTransaction.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php b/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php index 7b48022119..1822810c05 100644 --- a/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php +++ b/src/applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php @@ -9,4 +9,14 @@ final class PhabricatorDashboardTextPanelTextTransaction return 'text'; } + public function newRemarkupChanges() { + $changes = array(); + + $changes[] = $this->newRemarkupChange() + ->setOldValue($this->getOldValue()) + ->setNewValue($this->getNewValue()); + + return $changes; + } + } From 1996b0cd55c5f43a4fdbb0679deebd80ebc0adc7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Nov 2019 21:40:10 -0800 Subject: [PATCH 38/57] Update the "owner can always view/edit" policy exception rule Summary: Fixes T13460. This rule vanished from the UI in D20165; update things so it returns to the UI. Test Plan: {F7035134} Maniphest Tasks: T13460 Differential Revision: https://secure.phabricator.com/D20917 --- .../maniphest/policy/ManiphestTaskPolicyCodex.php | 9 +++++++++ src/applications/maniphest/storage/ManiphestTask.php | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php index 638d9bfa60..4394331541 100644 --- a/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php +++ b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php @@ -39,6 +39,15 @@ final class ManiphestTaskPolicyCodex $rules = array(); + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setDescription( + pht('The owner of a task can always view and edit it.')); + $rules[] = $this->newRule() ->setCapabilities( array( diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index c56d8fe57a..4f90de3507 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -358,10 +358,6 @@ final class ManiphestTask extends ManiphestDAO return false; } - public function describeAutomaticCapability($capability) { - return pht('The owner of a task can always view and edit it.'); - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ From de66a8ece185b6769bc5b55d82ea8a11a1b18a17 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Nov 2019 21:53:33 -0800 Subject: [PATCH 39/57] Remove "stronger/weaker" policy color hints from object headers Summary: Fixes T13461. Some applications provide hints about policy strength in the header, but these hints are inconsistent and somewhat confusing. They don't make much sense for modern objects with Custom Forms, which don't have a single "default" policy. Remove this feature since it seems to be confusing things more than illuminating them. Test Plan: - Viewed various objects, no longer saw colored policy hints. - Grepped for all removed symbols. Maniphest Tasks: T13461 Differential Revision: https://secure.phabricator.com/D20918 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 2 - .../codex/PhrictionDocumentPolicyCodex.php | 17 ----- .../policy/codex/PhabricatorPolicyCodex.php | 4 -- .../PhabricatorPolicyStrengthConstants.php | 9 --- .../PhabricatorPolicyExplainController.php | 68 ------------------- src/view/phui/PHUIHeaderView.php | 47 ------------- webroot/rsrc/css/phui/phui-header-view.css | 27 -------- 8 files changed, 3 insertions(+), 177 deletions(-) delete mode 100644 src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ffe4024ed7..23f002e36b 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '77de226f', + 'core.pkg.css' => 'b88ac037', 'core.pkg.js' => '705aec2c', 'differential.pkg.css' => '607c84be', 'differential.pkg.js' => '1b97518d', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => '01b796c0', 'rsrc/css/phui/phui-form.css' => '159e2d9c', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', - 'rsrc/css/phui/phui-header-view.css' => 'b500eeea', + 'rsrc/css/phui/phui-header-view.css' => 'be09cc83', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', 'rsrc/css/phui/phui-icon.css' => '4cbc684a', @@ -843,7 +843,7 @@ return array( 'phui-form-css' => '159e2d9c', 'phui-form-view-css' => '01b796c0', 'phui-head-thing-view-css' => 'd7f293df', - 'phui-header-view-css' => 'b500eeea', + 'phui-header-view-css' => 'be09cc83', 'phui-hovercard' => '074f0783', 'phui-hovercard-view-css' => '6ca90fa0', 'phui-icon-set-selector-css' => '7aa5f3ec', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0927f450b2..682dfff366 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4218,7 +4218,6 @@ phutil_register_library_map(array( 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', 'PhabricatorPolicyRulesView' => 'applications/policy/view/PhabricatorPolicyRulesView.php', 'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php', - 'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', @@ -10730,7 +10729,6 @@ phutil_register_library_map(array( 'PhabricatorPolicyRule' => 'Phobject', 'PhabricatorPolicyRulesView' => 'AphrontView', 'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension', - 'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => array( 'Phobject', diff --git a/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php b/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php index 748bec705b..f4536cad7c 100644 --- a/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php +++ b/src/applications/phriction/codex/PhrictionDocumentPolicyCodex.php @@ -41,23 +41,6 @@ final class PhrictionDocumentPolicyCodex ->executeOne(); } - public function compareToDefaultPolicy(PhabricatorPolicy $policy) { - $root_policy = $this->getDefaultPolicy(); - $strongest_policy = $this->getStrongestPolicy(); - - // Note that we never return 'weaker', because Phriction documents can - // never have weaker permissions than their parents. If this object has - // been set to weaker permissions anyway, return 'adjusted'. - if ($root_policy == $strongest_policy) { - $strength = null; - } else if ($strongest_policy->isStrongerThan($root_policy)) { - $strength = PhabricatorPolicyStrengthConstants::STRONGER; - } else { - $strength = PhabricatorPolicyStrengthConstants::ADJUSTED; - } - return $strength; - } - private function getStrongestPolicy() { $ancestors = $this->getObject()->getAncestors(); $ancestors[] = $this->getObject(); diff --git a/src/applications/policy/codex/PhabricatorPolicyCodex.php b/src/applications/policy/codex/PhabricatorPolicyCodex.php index 8dee2a38d1..9bccb842bb 100644 --- a/src/applications/policy/codex/PhabricatorPolicyCodex.php +++ b/src/applications/policy/codex/PhabricatorPolicyCodex.php @@ -40,10 +40,6 @@ abstract class PhabricatorPolicyCodex $this->capability); } - public function compareToDefaultPolicy(PhabricatorPolicy $policy) { - return null; - } - final protected function newRule() { return new PhabricatorPolicyCodexRuleDescription(); } diff --git a/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php b/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php deleted file mode 100644 index 9bc3c81ca2..0000000000 --- a/src/applications/policy/constants/PhabricatorPolicyStrengthConstants.php +++ /dev/null @@ -1,9 +0,0 @@ -getViewer(); - - - $strength = null; - if ($object instanceof PhabricatorPolicyCodexInterface) { - $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer)) - ->setCapability($capability); - $strength = $codex->compareToDefaultPolicy($policy); - $default_policy = $codex->getDefaultPolicy(); - } else { - $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject( - $viewer, - $object, - $capability); - - if ($default_policy) { - if ($default_policy->getPHID() == $policy->getPHID()) { - return; - } - - if ($default_policy->getPHID() != $policy->getPHID()) { - if ($default_policy->isStrongerThan($policy)) { - $strength = PhabricatorPolicyStrengthConstants::WEAKER; - } else if ($policy->isStrongerThan($default_policy)) { - $strength = PhabricatorPolicyStrengthConstants::STRONGER; - } else { - $strength = PhabricatorPolicyStrengthConstants::ADJUSTED; - } - } - } - } - - if (!$strength) { - return; - } - - if ($strength == PhabricatorPolicyStrengthConstants::WEAKER) { - $info = pht( - 'This object has a less restrictive policy ("%s") than the default '. - 'policy for similar objects (which is "%s").', - $policy->getShortName(), - $default_policy->getShortName()); - } else if ($strength == PhabricatorPolicyStrengthConstants::STRONGER) { - $info = pht( - 'This object has a more restrictive policy ("%s") than the default '. - 'policy for similar objects (which is "%s").', - $policy->getShortName(), - $default_policy->getShortName()); - } else { - $info = pht( - 'This object has a different policy ("%s") than the default policy '. - 'for similar objects (which is "%s").', - $policy->getShortName(), - $default_policy->getShortName()); - } - - return $info; - } - private function getCapabilityName($capability) { $capability_name = $capability; $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); @@ -344,11 +281,6 @@ final class PhabricatorPolicyExplainController $object_section->appendRulesView($rules_view); } - $strength = $this->getStrengthInformation($object, $policy, $capability); - if ($strength) { - $object_section->appendHint($strength); - } - return $object_section; } diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 54f4fa58ee..465768ae16 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -469,53 +469,6 @@ final class PHUIHeaderView extends AphrontTagView { $container_classes[] = 'policy-header-callout'; $phid = $object->getPHID(); - // If we're going to show the object policy, try to determine if the object - // policy differs from the default policy. If it does, we'll call it out - // as changed. - if (!$use_space_policy) { - $strength = null; - if ($object instanceof PhabricatorPolicyCodexInterface) { - $codex = id(PhabricatorPolicyCodex::newFromObject($object, $viewer)) - ->setCapability($view_capability); - $strength = $codex->compareToDefaultPolicy($policy); - } else { - $default_policy = PhabricatorPolicyQuery::getDefaultPolicyForObject( - $viewer, - $object, - $view_capability); - - if ($default_policy) { - if ($default_policy->getPHID() != $policy->getPHID()) { - if ($default_policy->isStrongerThan($policy)) { - $strength = PhabricatorPolicyStrengthConstants::WEAKER; - } else if ($policy->isStrongerThan($default_policy)) { - $strength = PhabricatorPolicyStrengthConstants::STRONGER; - } else { - $strength = PhabricatorPolicyStrengthConstants::ADJUSTED; - } - } - } - } - - if ($strength) { - if ($strength == PhabricatorPolicyStrengthConstants::WEAKER) { - // The policy has strictly been weakened. For example, the - // default might be "All Users" and the current policy is "Public". - $container_classes[] = 'policy-adjusted-weaker'; - } else if ($strength == PhabricatorPolicyStrengthConstants::STRONGER) { - // The policy has strictly been strengthened, and is now more - // restrictive than the default. For example, "All Users" has - // been replaced with "No One". - $container_classes[] = 'policy-adjusted-stronger'; - } else { - // The policy has been adjusted but not strictly strengthened - // or weakened. For example, "Members of X" has been replaced with - // "Members of Y". - $container_classes[] = 'policy-adjusted-different'; - } - } - } - $policy_name = array($policy->getShortName()); $policy_icon = $policy->getIcon().' bluegrey'; diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 1d851f04ee..e621d38134 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -213,33 +213,6 @@ body .phui-header-shell.phui-bleed-header -webkit-font-smoothing: auto; } -.policy-header-callout.policy-adjusted-weaker { - background: {$sh-greenbackground}; -} - -.policy-header-callout.policy-adjusted-weaker .policy-link, -.policy-header-callout.policy-adjusted-weaker .phui-icon-view { - color: {$sh-greentext}; -} - -.policy-header-callout.policy-adjusted-stronger { - background: {$sh-redbackground}; -} - -.policy-header-callout.policy-adjusted-stronger .policy-link, -.policy-header-callout.policy-adjusted-stronger .phui-icon-view { - color: {$sh-redtext}; -} - -.policy-header-callout.policy-adjusted-different { - background: {$sh-orangebackground}; -} - -.policy-header-callout.policy-adjusted-different .policy-link, -.policy-header-callout.policy-adjusted-different .phui-icon-view { - color: {$sh-orangetext}; -} - .policy-header-callout.policy-adjusted-special { background: {$sh-indigobackground}; } From 959504a4881cc238d81208244e6801b9afb087ca Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Nov 2019 22:21:15 -0800 Subject: [PATCH 40/57] When predicting object policies for project milestones, adjust objects so they behave like milestones Summary: Ref T13462. Currently, when testing milestone edit policies during creation, the project object does not behave like a milestone: - it doesn't have a milestone number yet, so it doesn't try to access the parent project; and - the parent project isn't attached yet. Instead: attach the parent project sooner (which "should" be harmless, although it's possible this has weird side effects); and give the adjusted policy object a dummy milestone number if it doesn't have one yet. This forces it to act like a milestone when emitting policies. Test Plan: - Set "Projects" application default edit policy to "No One". - Created a milestone I had permission to create. - Before: failed with a policy error, because the project behaved like a non-milestone and returned "No One" as the effective edit policy. - After: worked properly, correctly evaluting the parent project edit policy as the effective edit policy. - Tried to create a milestone I did not have permission to create (no edit permission on parent project). - Got an appropriate edit policy error. Maniphest Tasks: T13462 Differential Revision: https://secure.phabricator.com/D20919 --- .../project/__tests__/PhabricatorProjectCoreTestCase.php | 3 +-- .../editor/PhabricatorProjectTransactionEditor.php | 9 +++++++++ src/applications/project/storage/PhabricatorProject.php | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index 186ac7dea4..a31bf8853c 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -1537,8 +1537,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { PhabricatorProject $parent = null, $is_milestone = false) { - $project = PhabricatorProject::initializeNewProject($user); - + $project = PhabricatorProject::initializeNewProject($user, $parent); $name = pht('Test Project %d', mt_rand()); diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index b714f66830..729674e120 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -336,6 +336,15 @@ final class PhabricatorProjectTransactionEditor $type_edge = PhabricatorTransactions::TYPE_EDGE; $edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + // See T13462. If we're creating a milestone, set a dummy milestone + // number so the project behaves like a milestone and uses milestone + // policy rules. Otherwise, we'll end up checking the default policies + // (which are not relevant to milestones) instead of the parent project + // policies (which are the correct policies). + if ($this->getIsMilestone() && !$copy->isMilestone()) { + $copy->setMilestoneNumber(1); + } + $member_xaction = null; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() !== $type_edge) { diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 765c8a6b43..860d6e1749 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -107,7 +107,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO ->setHasMilestones(0) ->setHasSubprojects(0) ->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT) - ->attachParentProject(null); + ->attachParentProject($parent); } public function getCapabilities() { From d58eddcf0ad4bec4762632a351f6b604f0a6afb4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Nov 2019 22:49:40 -0800 Subject: [PATCH 41/57] When predicting project membership during edits, predict milestones will have parent membership Summary: Depends on D20919. Ref T13462. When editing milestones, we currently predict they will have no members for policy evaluation purposes. This isn't the right rule. Instead, predict that their membership will be the same as the parent project's membership, and pass this hint to the policy layer. See T13462 for additional context and discussion. Test Plan: - Set project A's edit policy to "Project Members". - Joined project A. - Tried to create a milestone of project A. - Before: policy exception that the edit policy excludes me. - After: clean milestone creation. - As a non-member, tried to create a milestone. Received appropriate policy error. Maniphest Tasks: T13462 Differential Revision: https://secure.phabricator.com/D20920 --- .../PhabricatorProjectTransactionEditor.php | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 729674e120..eb57c39b2c 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -345,40 +345,59 @@ final class PhabricatorProjectTransactionEditor $copy->setMilestoneNumber(1); } - $member_xaction = null; - foreach ($xactions as $xaction) { - if ($xaction->getTransactionType() !== $type_edge) { - continue; + $hint = null; + if ($this->getIsMilestone()) { + // See T13462. If we're creating a milestone, predict that the members + // of the newly created milestone will be the same as the members of the + // parent project, since this is the governing rule. + + $parent = $copy->getParentProject(); + + $parent = id(new PhabricatorProjectQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($parent->getPHID())) + ->needMembers(true) + ->executeOne(); + $members = $parent->getMemberPHIDs(); + + $hint = array_fuse($members); + } else { + $member_xaction = null; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() !== $type_edge) { + continue; + } + + $edgetype = $xaction->getMetadataValue('edge:type'); + if ($edgetype !== $edgetype_member) { + continue; + } + + $member_xaction = $xaction; } - $edgetype = $xaction->getMetadataValue('edge:type'); - if ($edgetype !== $edgetype_member) { - continue; - } + if ($member_xaction) { + $object_phid = $object->getPHID(); - $member_xaction = $xaction; + if ($object_phid) { + $project = id(new PhabricatorProjectQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($object_phid)) + ->needMembers(true) + ->executeOne(); + $members = $project->getMemberPHIDs(); + } else { + $members = array(); + } + + $clone_xaction = clone $member_xaction; + $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); + $hint = array_fuse($hint); + } } - if ($member_xaction) { - $object_phid = $object->getPHID(); - - if ($object_phid) { - $project = id(new PhabricatorProjectQuery()) - ->setViewer($this->getActor()) - ->withPHIDs(array($object_phid)) - ->needMembers(true) - ->executeOne(); - $members = $project->getMemberPHIDs(); - } else { - $members = array(); - } - - $clone_xaction = clone $member_xaction; - $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); + if ($hint !== null) { $rule = new PhabricatorProjectMembersPolicyRule(); - - $hint = array_fuse($hint); - PhabricatorPolicyRule::passTransactionHintToRule( $copy, $rule, From df0f5c6cee03b507a0dc0395ff39375bc41f8dcc Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Nov 2019 18:42:23 -0800 Subject: [PATCH 42/57] Make repository identity email address association case-insensitive Summary: Ref T13444. Currently, identities for a particular email address are queried with "LIKE" against a binary column, which makes the query case-sensitive. - Extract the email address into a separate "sort255" column. - Add a key for it. - Make the query a standard "IN (%Ls)" query. - Deal with weird cases where an email address is 10000 bytes long or full of binary junk. Test Plan: - Ran migration, inspected database for general sanity. - Ran query script in T13444, saw it return the same hits for "git@" and "GIT@". Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20907 --- .../20191113.identity.01.email.sql | 2 ++ .../20191113.identity.02.populate.php | 26 ++++++++++++++++ .../DiffusionIdentityViewController.php | 15 +++++---- .../DiffusionRepositoryListController.php | 8 +++++ .../PhabricatorRepositoryIdentityQuery.php | 13 ++++---- .../storage/PhabricatorRepositoryIdentity.php | 31 ++++++++++++++++++- ...bricatorRepositoryIdentityChangeWorker.php | 2 +- 7 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 resources/sql/autopatches/20191113.identity.01.email.sql create mode 100644 resources/sql/autopatches/20191113.identity.02.populate.php diff --git a/resources/sql/autopatches/20191113.identity.01.email.sql b/resources/sql/autopatches/20191113.identity.01.email.sql new file mode 100644 index 0000000000..938e6c0767 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.01.email.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_identity + ADD emailAddress VARCHAR(255) COLLATE {$COLLATE_SORT}; diff --git a/resources/sql/autopatches/20191113.identity.02.populate.php b/resources/sql/autopatches/20191113.identity.02.populate.php new file mode 100644 index 0000000000..ca86be0733 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.02.populate.php @@ -0,0 +1,26 @@ +establishConnection('w'); + +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); +foreach ($iterator as $row) { + $name = $row['identityNameRaw']; + $name = phutil_utf8ize($name); + + $email = new PhutilEmailAddress($name); + $address = $email->getAddress(); + + try { + queryfx( + $conn, + 'UPDATE %R SET emailAddress = %ns WHERE id = %d', + $table, + $address, + $row['id']); + } catch (Exception $ex) { + // We may occasionally run into issues with binary or very long addresses. + // Just skip over them. + continue; + } +} diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php index 20efe2749f..0ac131daf1 100644 --- a/src/applications/diffusion/controller/DiffusionIdentityViewController.php +++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php @@ -22,11 +22,10 @@ final class DiffusionIdentityViewController $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($identity->getIdentityShortName()) - ->setHeaderIcon('fa-globe') - ->setPolicyObject($identity); + ->setHeaderIcon('fa-globe'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($identity->getID()); + $crumbs->addTextCrumb($identity->getObjectName()); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( @@ -83,7 +82,11 @@ final class DiffusionIdentityViewController $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($viewer); + ->setViewer($viewer); + + $properties->addProperty( + pht('Email Address'), + $identity->getEmailAddress()); $effective_phid = $identity->getCurrentEffectiveUserPHID(); $automatic_phid = $identity->getAutomaticGuessedUserPHID(); @@ -109,7 +112,7 @@ final class DiffusionIdentityViewController pht('Automatically Detected User'), $this->buildPropertyValue($automatic_phid)); $properties->addProperty( - pht('Manually Set User'), + pht('Assigned To'), $this->buildPropertyValue($manual_phid)); $header = id(new PHUIHeaderView()) @@ -127,7 +130,7 @@ final class DiffusionIdentityViewController if ($value == DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN) { return phutil_tag('em', array(), pht('Explicitly Unassigned')); } else if (!$value) { - return null; + return phutil_tag('em', array(), pht('None')); } else { return $viewer->renderHandle($value); } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryListController.php b/src/applications/diffusion/controller/DiffusionRepositoryListController.php index 5a21d2e3f1..66226e5eab 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryListController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryListController.php @@ -17,6 +17,14 @@ final class DiffusionRepositoryListController extends DiffusionController { ->setName(pht('Browse Commits')) ->setHref($this->getApplicationURI('commit/')); + $items[] = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Identities')); + + $items[] = id(new PHUIListItemView()) + ->setName(pht('Browse Identities')) + ->setHref($this->getApplicationURI('identity/')); + return id(new PhabricatorRepositorySearchEngine()) ->setController($this) ->setNavigationItems($items) diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index c64b1a296b..6ddf8d57c1 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -6,7 +6,7 @@ final class PhabricatorRepositoryIdentityQuery private $ids; private $phids; private $identityNames; - private $emailAddress; + private $emailAddresses; private $assigneePHIDs; private $identityNameLike; private $hasEffectivePHID; @@ -31,8 +31,8 @@ final class PhabricatorRepositoryIdentityQuery return $this; } - public function withEmailAddress($address) { - $this->emailAddress = $address; + public function withEmailAddresses(array $addresses) { + $this->emailAddresses = $addresses; return $this; } @@ -106,12 +106,11 @@ final class PhabricatorRepositoryIdentityQuery $name_hashes); } - if ($this->emailAddress !== null) { - $identity_style = "<{$this->emailAddress}>"; + if ($this->emailAddresses !== null) { $where[] = qsprintf( $conn, - 'repository_identity.identityNameRaw LIKE %<', - $identity_style); + 'repository_identity.emailAddress IN (%Ls)', + $this->emailAddresses); } if ($this->identityNameLike != null) { diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index e3833bd10e..c33b296fdc 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -13,6 +13,7 @@ final class PhabricatorRepositoryIdentity protected $automaticGuessedUserPHID; protected $manuallySetUserPHID; protected $currentEffectiveUserPHID; + protected $emailAddress; protected function getConfiguration() { return array( @@ -26,12 +27,16 @@ final class PhabricatorRepositoryIdentity 'automaticGuessedUserPHID' => 'phid?', 'manuallySetUserPHID' => 'phid?', 'currentEffectiveUserPHID' => 'phid?', + 'emailAddress' => 'sort255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_identity' => array( 'columns' => array('identityNameHash'), 'unique' => true, ), + 'key_email' => array( + 'columns' => array('emailAddress(64)'), + ), ), ) + parent::getConfiguration(); } @@ -69,6 +74,10 @@ final class PhabricatorRepositoryIdentity return $this->getIdentityName(); } + public function getObjectName() { + return pht('Identity %d', $this->getID()); + } + public function getURI() { return '/diffusion/identity/view/'.$this->getID().'/'; } @@ -92,6 +101,25 @@ final class PhabricatorRepositoryIdentity $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; } + $email_address = $this->getIdentityEmailAddress(); + + // Raw identities are unrestricted binary data, and may consequently + // have arbitrarily long, binary email address information. We can't + // store this kind of information in the "emailAddress" column, which + // has column type "sort255". + + // This kind of address almost certainly not legitimate and users can + // manually set the target of the identity, so just discard it rather + // than trying especially hard to make it work. + + $byte_limit = $this->getColumnMaximumByteLength('emailAddress'); + $email_address = phutil_utf8ize($email_address); + if (strlen($email_address) > $byte_limit) { + $email_address = null; + } + + $this->setEmailAddress($email_address); + return parent::save(); } @@ -111,7 +139,8 @@ final class PhabricatorRepositoryIdentity } public function hasAutomaticCapability( - $capability, PhabricatorUser $viewer) { + $capability, + PhabricatorUser $viewer) { return false; } diff --git a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php index 3c129845cd..0ce01ce76a 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php @@ -21,7 +21,7 @@ extends PhabricatorWorker { foreach ($emails as $email) { $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) - ->withEmailAddress($email->getAddress()) + ->withEmailAddresses($email->getAddress()) ->execute(); foreach ($identities as $identity) { From a2b2c391a1f151f3720324b8c141b96b7a931c96 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Nov 2019 19:24:48 -0800 Subject: [PATCH 43/57] Distinguish between "Assigned" and "Effective" identity PHIDs more clearly and consistently Summary: Ref T13444. You can currently explicitly unassign an identity (useful if the matching algorithm is misfiring). However, this populates the "currentEffectiveUserPHID" with the "unassigned()" token, which mostly makes things more difficult. When an identity is explicitly unassigned, convert that into an explicit `null` in the effective user PHID. Then, realign "assigned" / "effective" language a bit. Previously, `withAssigneePHIDs(...)` actualy queried effective users, which was misleading. Finally, bulk up the list view a little bit to make testing slightly easier. Test Plan: - Unassigned an identity, ran migration, saw `currentEffectiveUserPHID` become `NULL` for the identity. - Unassigned a fresh identity, saw NULL. - Queried for various identities under the modified constraints. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20908 --- .../20191113.identity.03.unassigned.sql | 3 + .../DiffusionIdentityViewController.php | 3 + ...iffusionRepositoryIdentitySearchEngine.php | 77 ++++++++++++++++--- .../PhabricatorRepositoryIdentityQuery.php | 23 ++++-- .../storage/PhabricatorRepositoryIdentity.php | 11 ++- 5 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 resources/sql/autopatches/20191113.identity.03.unassigned.sql diff --git a/resources/sql/autopatches/20191113.identity.03.unassigned.sql b/resources/sql/autopatches/20191113.identity.03.unassigned.sql new file mode 100644 index 0000000000..768ca1d909 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.03.unassigned.sql @@ -0,0 +1,3 @@ +UPDATE {$NAMESPACE}_repository.repository_identity + SET currentEffectiveUserPHID = NULL + WHERE currentEffectiveUserPHID = 'unassigned()'; diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php index 0ac131daf1..1c78ec5992 100644 --- a/src/applications/diffusion/controller/DiffusionIdentityViewController.php +++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php @@ -25,6 +25,9 @@ final class DiffusionIdentityViewController ->setHeaderIcon('fa-globe'); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Identities'), + $this->getApplicationURI('identity/')); $crumbs->addTextCrumb($identity->getObjectName()); $crumbs->setBorder(true); diff --git a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php index 4d10ee44e0..f41b6b89a4 100644 --- a/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php +++ b/src/applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php @@ -17,21 +17,35 @@ final class DiffusionRepositoryIdentitySearchEngine protected function buildCustomSearchFields() { return array( + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Matching Users')) + ->setKey('effectivePHIDs') + ->setAliases( + array( + 'effective', + 'effectivePHID', + )) + ->setDescription(pht('Search for identities by effective user.')), id(new DiffusionIdentityAssigneeSearchField()) ->setLabel(pht('Assigned To')) - ->setKey('assignee') - ->setDescription(pht('Search for identities by assignee.')), + ->setKey('assignedPHIDs') + ->setAliases( + array( + 'assigned', + 'assignedPHID', + )) + ->setDescription(pht('Search for identities by explicit assignee.')), id(new PhabricatorSearchTextField()) ->setLabel(pht('Identity Contains')) ->setKey('match') ->setDescription(pht('Search for identities by substring.')), id(new PhabricatorSearchThreeStateField()) - ->setLabel(pht('Is Assigned')) + ->setLabel(pht('Has Matching User')) ->setKey('hasEffectivePHID') ->setOptions( pht('(Show All)'), - pht('Show Only Assigned Identities'), - pht('Show Only Unassigned Identities')), + pht('Show Identities With Matching Users'), + pht('Show Identities Without Matching Users')), ); } @@ -46,8 +60,12 @@ final class DiffusionRepositoryIdentitySearchEngine $query->withIdentityNameLike($map['match']); } - if ($map['assignee']) { - $query->withAssigneePHIDs($map['assignee']); + if ($map['assignedPHIDs']) { + $query->withAssignedPHIDs($map['assignedPHIDs']); + } + + if ($map['effectivePHIDs']) { + $query->withEffectivePHIDs($map['effectivePHIDs']); } return $query; @@ -86,15 +104,54 @@ final class DiffusionRepositoryIdentitySearchEngine $viewer = $this->requireViewer(); - $list = new PHUIObjectItemListView(); - $list->setUser($viewer); + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + + $phids = array(); + foreach ($identities as $identity) { + $phids[] = $identity->getCurrentEffectiveUserPHID(); + $phids[] = $identity->getManuallySetUserPHID(); + } + + $handles = $viewer->loadHandles($phids); + + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + foreach ($identities as $identity) { $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Identity %d', $identity->getID())) + ->setObjectName($identity->getObjectName()) ->setHeader($identity->getIdentityShortName()) ->setHref($identity->getURI()) ->setObject($identity); + $status_icon = 'fa-circle-o grey'; + + $effective_phid = $identity->getCurrentEffectiveUserPHID(); + if ($effective_phid) { + $item->addIcon( + 'fa-id-badge', + pht('Matches User: %s', $handles[$effective_phid]->getName())); + + $status_icon = 'fa-id-badge'; + } + + $assigned_phid = $identity->getManuallySetUserPHID(); + if ($assigned_phid) { + if ($assigned_phid === $unassigned) { + $item->addIcon( + 'fa-ban', + pht('Explicitly Unassigned')); + $status_icon = 'fa-ban'; + } else { + $item->addIcon( + 'fa-user', + pht('Assigned To: %s', $handles[$assigned_phid]->getName())); + $status_icon = 'fa-user'; + } + } + + $item->setStatusIcon($status_icon); + $list->addItem($item); } diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index 6ddf8d57c1..14c2a75f80 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -7,7 +7,8 @@ final class PhabricatorRepositoryIdentityQuery private $phids; private $identityNames; private $emailAddresses; - private $assigneePHIDs; + private $assignedPHIDs; + private $effectivePHIDs; private $identityNameLike; private $hasEffectivePHID; @@ -36,8 +37,13 @@ final class PhabricatorRepositoryIdentityQuery return $this; } - public function withAssigneePHIDs(array $assignees) { - $this->assigneePHIDs = $assignees; + public function withAssignedPHIDs(array $assigned) { + $this->assignedPHIDs = $assigned; + return $this; + } + + public function withEffectivePHIDs(array $effective) { + $this->effectivePHIDs = $effective; return $this; } @@ -75,11 +81,18 @@ final class PhabricatorRepositoryIdentityQuery $this->phids); } - if ($this->assigneePHIDs !== null) { + if ($this->assignedPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'repository_identity.manuallySetUserPHID IN (%Ls)', + $this->assignedPHIDs); + } + + if ($this->effectivePHIDs !== null) { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IN (%Ls)', - $this->assigneePHIDs); + $this->effectivePHIDs); } if ($this->hasEffectivePHID !== null) { diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index c33b296fdc..74fbd06544 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -96,11 +96,18 @@ final class PhabricatorRepositoryIdentity public function save() { if ($this->manuallySetUserPHID) { - $this->currentEffectiveUserPHID = $this->manuallySetUserPHID; + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + if ($this->manuallySetUserPHID === $unassigned) { + $effective_phid = null; + } else { + $effective_phid = $this->manuallySetUserPHID; + } } else { - $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; + $effective_phid = $this->automaticGuessedUserPHID; } + $this->setCurrentEffectiveUserPHID($effective_phid); + $email_address = $this->getIdentityEmailAddress(); // Raw identities are unrestricted binary data, and may consequently From 6afbb6102ddacaebb7170cf4f9c26aef996028d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Nov 2019 20:22:48 -0800 Subject: [PATCH 44/57] Remove "PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER" event Summary: Ref T13444. This is an ancient event and part of the old event system. It is not likely to be in use anymore, and repository identities should generally replace it nowadays anyway. Test Plan: Grepped for constant and related methods, no longer found any hits. Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20909 --- .../query/DiffusionResolveUserQuery.php | 41 ++++--------------- ...epositoryManagementLookupUsersWorkflow.php | 1 - ...oryManagementRebuildIdentitiesWorkflow.php | 1 - ...torRepositoryCommitMessageParserWorker.php | 1 - src/docs/user/userguide/events.diviner | 29 ------------- .../events/constant/PhabricatorEventType.php | 1 - 6 files changed, 8 insertions(+), 66 deletions(-) diff --git a/src/applications/diffusion/query/DiffusionResolveUserQuery.php b/src/applications/diffusion/query/DiffusionResolveUserQuery.php index a22408ce16..8ad13f660e 100644 --- a/src/applications/diffusion/query/DiffusionResolveUserQuery.php +++ b/src/applications/diffusion/query/DiffusionResolveUserQuery.php @@ -8,25 +8,14 @@ final class DiffusionResolveUserQuery extends Phobject { private $name; - private $commit; public function withName($name) { $this->name = $name; return $this; } - public function withCommit($commit) { - $this->commit = $commit; - return $this; - } - public function execute() { - $user_name = $this->name; - - $phid = $this->findUserPHID($this->name); - $phid = $this->fireLookupEvent($phid); - - return $phid; + return $this->findUserPHID($this->name); } private function findUserPHID($user_name) { @@ -75,33 +64,15 @@ final class DiffusionResolveUserQuery extends Phobject { } - /** - * Emit an event so installs can do custom lookup of commit authors who may - * not be naturally resolvable. - */ - private function fireLookupEvent($guess) { - - $type = PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER; - $data = array( - 'commit' => $this->commit, - 'query' => $this->name, - 'result' => $guess, - ); - - $event = new PhabricatorEvent($type, $data); - PhutilEventEngine::dispatchEvent($event); - - return $event->getValue('result'); - } - - private function findUserByUserName($user_name) { $by_username = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user_name); + if ($by_username) { return $by_username->getPHID(); } + return null; } @@ -112,18 +83,22 @@ final class DiffusionResolveUserQuery extends Phobject { $by_realname = id(new PhabricatorUser())->loadAllWhere( 'realName = %s', $real_name); + if (count($by_realname) == 1) { - return reset($by_realname)->getPHID(); + return head($by_realname)->getPHID(); } + return null; } private function findUserByEmailAddress($email_address) { $by_email = PhabricatorUser::loadOneWithEmailAddress($email_address); + if ($by_email) { return $by_email->getPHID(); } + return null; } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php index e02a8dc05c..ec65a8bcfa 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php @@ -99,7 +99,6 @@ final class PhabricatorRepositoryManagementLookupUsersWorkflow private function resolveUser(PhabricatorRepositoryCommit $commit, $name) { $phid = id(new DiffusionResolveUserQuery()) - ->withCommit($commit) ->withName($name) ->execute(); diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index 86cdcaa462..02ab6e9bf8 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -101,7 +101,6 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow if (empty($seen[$identity_key])) { try { $user_phid = id(new DiffusionResolveUserQuery()) - ->withCommit($commit) ->withName($identity_name) ->execute(); diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index c7f00df73e..e1cc5c90eb 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -182,7 +182,6 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $user_name) { return id(new DiffusionResolveUserQuery()) - ->withCommit($commit) ->withName($user_name) ->execute(); } diff --git a/src/docs/user/userguide/events.diviner b/src/docs/user/userguide/events.diviner index dc9722a596..ea66448c8a 100644 --- a/src/docs/user/userguide/events.diviner +++ b/src/docs/user/userguide/events.diviner @@ -159,35 +159,6 @@ will be available yet. Data available on this event: - `repository` The @{class:PhabricatorRepository} the commit was discovered in. -== Diffusion: Lookup User == - -The constant for this event is -`PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER`. - -This event is dispatched when the daemons are trying to link a commit to a -Phabricator user account. You can listen for it to improve the accuracy of -associating users with their commits. - -By default, Phabricator will try to find matches based on usernames, real names, -or email addresses, but this can result in incorrect matches (e.g., if you have -several employees with the same name) or failures to match (e.g., if someone -changed their email address). Listening for this event allows you to intercept -the lookup and supplement the results from another datasource. - -Data available on this event: - - - `commit` The @{class:PhabricatorRepositoryCommit} that data is being looked - up for. - - `query` The author or committer string being looked up. This will usually - be something like "Abraham Lincoln ", but - comes from the commit metadata so it may not be well-formatted. - - `result` The current result from the lookup (Phabricator's best guess at - the user PHID of the user named in the "query"). To substitute the result - with a different result, replace this with the correct PHID in your event - listener. - -Using @{class@libphutil:PhutilEmailAddress} may be helpful in parsing the query. - == Test: Did Run Test == The constant for this event is diff --git a/src/infrastructure/events/constant/PhabricatorEventType.php b/src/infrastructure/events/constant/PhabricatorEventType.php index 3dea7b36e6..7d3a8981bf 100644 --- a/src/infrastructure/events/constant/PhabricatorEventType.php +++ b/src/infrastructure/events/constant/PhabricatorEventType.php @@ -9,7 +9,6 @@ final class PhabricatorEventType extends PhutilEventType { const TYPE_DIFFERENTIAL_WILLMARKGENERATED = 'differential.willMarkGenerated'; const TYPE_DIFFUSION_DIDDISCOVERCOMMIT = 'diffusion.didDiscoverCommit'; - const TYPE_DIFFUSION_LOOKUPUSER = 'diffusion.lookupUser'; const TYPE_TEST_DIDRUNTEST = 'test.didRunTest'; From 0014d0404c2d7da89a44e1cf9b4d66e71d597856 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Nov 2019 20:20:33 -0800 Subject: [PATCH 45/57] Consolidate repository identity resolution and detection code Summary: Ref T13444. Send all repository identity/detection through a new "DiffusionRepositoryIdentityEngine" which handles resolution and detection updates in one place. Test Plan: - Ran `bin/repository reparse --message ...`, saw author/committer identity updates. - Added "goose@example.com" to my email addresses, ran daemons, saw the identity relationship get picked up. - Ran `bin/repository rebuild-identities ...`, saw sensible rebuilds. Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20910 --- src/__phutil_library_map__.php | 2 + .../DiffusionRepositoryIdentityEngine.php | 80 +++++++++++++++++++ ...oryManagementRebuildIdentitiesWorkflow.php | 36 ++++----- ...bricatorRepositoryIdentityChangeWorker.php | 12 +-- ...torRepositoryCommitMessageParserWorker.php | 53 ++++-------- 5 files changed, 117 insertions(+), 66 deletions(-) create mode 100644 src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 682dfff366..9d85964435 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -985,6 +985,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php', 'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php', 'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php', + 'DiffusionRepositoryIdentityEngine' => 'applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php', 'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php', 'DiffusionRepositoryLimitsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryLimitsManagementPanel.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', @@ -6966,6 +6967,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor', + 'DiffusionRepositoryIdentityEngine' => 'Phobject', 'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine', 'DiffusionRepositoryLimitsManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryListController' => 'DiffusionController', diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php new file mode 100644 index 0000000000..5d71996622 --- /dev/null +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php @@ -0,0 +1,80 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setSourcePHID($source_phid) { + $this->sourcePHID = $source_phid; + return $this; + } + + public function getSourcePHID() { + if (!$this->sourcePHID) { + throw new PhutilInvalidStateException('setSourcePHID'); + } + + return $this->sourcePHID; + } + + public function newResolvedIdentity($raw_identity) { + $identity = $this->loadRawIdentity($raw_identity); + + if (!$identity) { + $identity = $this->newIdentity($raw_identity); + } + + return $this->updateIdentity($identity); + } + + public function newUpdatedIdentity(PhabricatorRepositoryIdentity $identity) { + return $this->updateIdentity($identity); + } + + private function loadRawIdentity($raw_identity) { + $viewer = $this->getViewer(); + + return id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withIdentityNames(array($raw_identity)) + ->executeOne(); + } + + private function newIdentity($raw_identity) { + $source_phid = $this->getSourcePHID(); + + return id(new PhabricatorRepositoryIdentity()) + ->setAuthorPHID($source_phid) + ->setIdentityName($raw_identity); + } + + private function resolveIdentity(PhabricatorRepositoryIdentity $identity) { + $raw_identity = $identity->getIdentityName(); + + return id(new DiffusionResolveUserQuery()) + ->withName($raw_identity) + ->execute(); + } + + private function updateIdentity(PhabricatorRepositoryIdentity $identity) { + $resolved_phid = $this->resolveIdentity($identity); + + $identity + ->setAutomaticGuessedUserPHID($resolved_phid) + ->save(); + + return $identity; + } + +} diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index 02ab6e9bf8..77ba5b55b0 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -3,6 +3,8 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow extends PhabricatorRepositoryManagementWorkflow { + private $identityCache = array(); + protected function didConstruct() { $this ->setName('rebuild-identities') @@ -94,31 +96,21 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow } private function getIdentityForCommit( - PhabricatorRepositoryCommit $commit, $identity_name) { + PhabricatorRepositoryCommit $commit, + $raw_identity) { - static $seen = array(); - $identity_key = PhabricatorHash::digestForIndex($identity_name); - if (empty($seen[$identity_key])) { - try { - $user_phid = id(new DiffusionResolveUserQuery()) - ->withName($identity_name) - ->execute(); + if (!isset($this->identityCache[$raw_identity])) { + $viewer = $this->getViewer(); - $identity = id(new PhabricatorRepositoryIdentity()) - ->setAuthorPHID($commit->getPHID()) - ->setIdentityName($identity_name) - ->setAutomaticGuessedUserPHID($user_phid) - ->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - // Somehow this identity already exists? - $identity = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIdentityNames(array($identity_name)) - ->executeOne(); - } - $seen[$identity_key] = $identity; + $identity = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer) + ->setSourcePHID($commit->getPHID()) + ->newResolvedIdentity($raw_identity); + + $this->identityCache[$raw_identity] = $identity; } - return $seen[$identity_key]; + return $this->identityCache[$raw_identity]; } + } diff --git a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php index 0ce01ce76a..3d5cac8a03 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php @@ -1,7 +1,7 @@ executeOne(); $emails = id(new PhabricatorUserEmail())->loadAllWhere( - 'userPHID = %s ORDER BY address', + 'userPHID = %s', $user->getPHID()); + $identity_engine = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer); + foreach ($emails as $email) { $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) - ->withEmailAddresses($email->getAddress()) + ->withEmailAddresses(array($email->getAddress())) ->execute(); foreach ($identities as $identity) { - $identity->setAutomaticGuessedUserPHID($user->getPHID()) - ->save(); + $identity_engine->newUpdatedIdentity($identity); } } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index e1cc5c90eb..fef1c2eeeb 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -65,37 +65,20 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $message = $ref->getMessage(); $committer = $ref->getCommitter(); $hashes = $ref->getHashes(); + $has_committer = (bool)strlen($committer); - $author_identity = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIdentityNames(array($author)) - ->executeOne(); + $viewer = PhabricatorUser::getOmnipotentUser(); - if (!$author_identity) { - $author_identity = id(new PhabricatorRepositoryIdentity()) - ->setAuthorPHID($commit->getPHID()) - ->setIdentityName($author) - ->setAutomaticGuessedUserPHID( - $this->resolveUserPHID($commit, $author)) - ->save(); - } + $identity_engine = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer) + ->setSourcePHID($commit->getPHID()); - $committer_identity = null; + $author_identity = $identity_engine->newResolvedIdentity($author); - if ($committer) { - $committer_identity = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIdentityNames(array($committer)) - ->executeOne(); - - if (!$committer_identity) { - $committer_identity = id(new PhabricatorRepositoryIdentity()) - ->setAuthorPHID($commit->getPHID()) - ->setIdentityName($committer) - ->setAutomaticGuessedUserPHID( - $this->resolveUserPHID($commit, $committer)) - ->save(); - } + if ($has_committer) { + $committer_identity = $identity_engine->newResolvedIdentity($committer); + } else { + $committer_identity = null; } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( @@ -117,11 +100,11 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker 'authorIdentityPHID', $author_identity->getPHID()); $data->setCommitDetail( 'authorPHID', - $this->resolveUserPHID($commit, $author)); + $author_identity->getCurrentEffectiveUserPHID()); $data->setCommitMessage($message); - if (strlen($committer)) { + if ($has_committer) { $data->setCommitDetail('committer', $committer); $data->setCommitDetail('committerName', $ref->getCommitterName()); @@ -129,7 +112,8 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker $data->setCommitDetail( 'committerPHID', - $this->resolveUserPHID($commit, $committer)); + $committer_identity->getCurrentEffectiveUserPHID()); + $data->setCommitDetail( 'committerIdentityPHID', $committer_identity->getPHID()); @@ -177,15 +161,6 @@ abstract class PhabricatorRepositoryCommitMessageParserWorker PhabricatorRepositoryCommit::IMPORTED_MESSAGE); } - private function resolveUserPHID( - PhabricatorRepositoryCommit $commit, - $user_name) { - - return id(new DiffusionResolveUserQuery()) - ->withName($user_name) - ->execute(); - } - private function closeRevisions( PhabricatorUser $actor, DiffusionCommitRef $ref, From 18da346972b65d04757bf0244272bb16e73272fb Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Nov 2019 18:28:20 -0800 Subject: [PATCH 46/57] Add additional flags to "bin/repository rebuild-identities" to improve flexibility Summary: Ref T13444. Repository identities have, at a minimum, some bugs where they do not update relationships properly after many types of email address changes. It is currently very difficult to fix this once the damage is done since there's no good way to inspect or rebuild them. Take some steps toward improving observability and providing repair tools: allow `bin/repository rebuild-identities` to effect more repairs and operate on identities more surgically. Test Plan: Ran `bin/repository rebuild-identities` with all new flags, saw what looked like reasonable rebuilds occur. Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20911 --- .../PhabricatorManualActivitySetupCheck.php | 3 +- .../DiffusionRepositoryIdentityEngine.php | 18 + ...oryManagementRebuildIdentitiesWorkflow.php | 317 ++++++++++++++++-- .../PhabricatorRepositoryIdentityQuery.php | 37 +- .../PhabricatorManagementWorkflow.php | 121 +++++++ 5 files changed, 460 insertions(+), 36 deletions(-) diff --git a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php index c5c756e49b..04457cb12a 100644 --- a/src/applications/config/check/PhabricatorManualActivitySetupCheck.php +++ b/src/applications/config/check/PhabricatorManualActivitySetupCheck.php @@ -113,7 +113,8 @@ final class PhabricatorManualActivitySetupCheck 'pre', array(), (string)csprintf( - 'phabricator/ $ ./bin/repository rebuild-identities --all')); + 'phabricator/ $ '. + './bin/repository rebuild-identities --all-repositories')); $message[] = pht( 'You can find more information about this new identity mapping '. diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php index 5d71996622..635622fda2 100644 --- a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php @@ -68,6 +68,24 @@ final class DiffusionRepositoryIdentityEngine } private function updateIdentity(PhabricatorRepositoryIdentity $identity) { + + // If we're updating an identity and it has a manual user PHID associated + // with it but the user is no longer valid, remove the value. This likely + // corresponds to a user that was destroyed. + + $assigned_phid = $identity->getManuallySetUserPHID(); + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; + if ($assigned_phid && ($assigned_phid !== $unassigned)) { + $viewer = $this->getViewer(); + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($assigned_phid)) + ->executeOne(); + if (!$user) { + $identity->setManuallySetUserPHID(null); + } + } + $resolved_phid = $this->resolveIdentity($identity); $identity diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index 77ba5b55b0..c1f92e8371 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -14,38 +14,172 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow ->setArguments( array( array( - 'name' => 'repositories', - 'wildcard' => true, + 'name' => 'all-repositories', + 'help' => pht('Rebuild identities across all repositories.'), ), array( - 'name' => 'all', - 'help' => pht('Rebuild identities across all repositories.'), - ), + 'name' => 'all-identities', + 'help' => pht('Rebuild all currently-known identities.'), + ), + array( + 'name' => 'repository', + 'param' => 'repository', + 'repeat' => true, + 'help' => pht('Rebuild identities in a repository.'), + ), + array( + 'name' => 'commit', + 'param' => 'commit', + 'repeat' => true, + 'help' => pht('Rebuild identities for a commit.'), + ), + array( + 'name' => 'user', + 'param' => 'user', + 'repeat' => true, + 'help' => pht('Rebuild identities for a user.'), + ), + array( + 'name' => 'email', + 'param' => 'email', + 'repeat' => true, + 'help' => pht('Rebuild identities for an email address.'), + ), + array( + 'name' => 'raw', + 'param' => 'raw', + 'repeat' => true, + 'help' => pht('Rebuild identities for a raw commit string.'), + ), )); } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); + $viewer = $this->getViewer(); - $all = $args->getArg('all'); - $repositories = $args->getArg('repositories'); + $rebuilt_anything = false; - if ($all xor empty($repositories)) { + + $all_repositories = $args->getArg('all-repositories'); + $repositories = $args->getArg('repository'); + + if ($all_repositories && $repositories) { throw new PhutilArgumentUsageException( - pht('Specify --all or a list of repositories, but not both.')); + pht( + 'Flags "--all-repositories" and "--repository" are not '. + 'compatible.')); } - $query = id(new DiffusionCommitQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->needCommitData(true); - if ($repositories) { - $repos = $this->loadRepositories($args, 'repositories'); - $query->withRepositoryIDs(mpull($repos, 'getID')); + $all_identities = $args->getArg('all-identities'); + $raw = $args->getArg('raw'); + + if ($all_identities && $raw) { + throw new PhutilArgumentUsageException( + pht( + 'Flags "--all-identities" and "--raw" are not '. + 'compatible.')); } - $iterator = new PhabricatorQueryIterator($query); - foreach ($iterator as $commit) { + if ($all_repositories || $repositories) { + $rebuilt_anything = true; + + if ($repositories) { + $repository_list = $this->loadRepositories($args, 'repository'); + } else { + $repository_query = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer); + $repository_list = new PhabricatorQueryIterator($repository_query); + } + + foreach ($repository_list as $repository) { + $commit_query = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->needCommitData(true) + ->withRepositoryIDs(array($repository->getID())); + + $commit_iterator = new PhabricatorQueryIterator($commit_query); + + $this->rebuildCommits($commit_iterator); + } + } + + $commits = $args->getArg('commit'); + if ($commits) { + $rebuilt_anything = true; + $commit_list = $this->loadCommits($args, 'commit'); + + // Reload commits to get commit data. + $commit_list = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->needCommitData(true) + ->withIDs(mpull($commit_list, 'getID')) + ->execute(); + + $this->rebuildCommits($commit_list); + } + + $users = $args->getArg('user'); + if ($users) { + $rebuilt_anything = true; + + $user_list = $this->loadUsersFromArguments($users); + $this->rebuildUsers($user_list); + } + + $emails = $args->getArg('email'); + if ($emails) { + $rebuilt_anything = true; + $this->rebuildEmails($emails); + } + + if ($all_identities || $raw) { + $rebuilt_anything = true; + + if ($raw) { + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withIdentityNames($raw) + ->execute(); + + $identities = mpull($identities, null, 'getIdentityNameRaw'); + foreach ($raw as $raw_identity) { + if (!isset($identities[$raw_identity])) { + throw new PhutilArgumentUsageException( + pht( + 'No identity "%s" exists. When selecting identities with '. + '"--raw", the entire identity must match exactly.', + $raw_identity)); + } + } + + $identity_list = $identities; + } else { + $identity_query = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer); + + $identity_list = new PhabricatorQueryIterator($identity_query); + + $this->logInfo( + pht('REBUILD'), + pht('Rebuilding all existing identities.')); + } + + $this->rebuildIdentities($identity_list); + } + + if (!$rebuilt_anything) { + throw new PhutilArgumentUsageException( + pht( + 'Nothing specified to rebuild. Use flags to choose which '. + 'identities to rebuild, or "--help" for help.')); + } + + return 0; + } + + private function rebuildCommits($commits) { + foreach ($commits as $commit) { $needs_update = false; $data = $commit->getCommitData(); @@ -57,6 +191,8 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow $author_phid = $commit->getAuthorIdentityPHID(); $identity_phid = $author_identity->getPHID(); + + $aidentity_phid = $identity_phid; if ($author_phid !== $identity_phid) { $commit->setAuthorIdentityPHID($identity_phid); $data->setCommitDetail('authorIdentityPHID', $identity_phid); @@ -83,16 +219,20 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow if ($needs_update) { $commit->save(); $data->save(); - echo tsprintf( - "Rebuilt identities for %s.\n", - $commit->getDisplayName()); + + $this->logInfo( + pht('COMMIT'), + pht( + 'Rebuilt identities for "%s".', + $commit->getDisplayName())); } else { - echo tsprintf( - "No changes for %s.\n", - $commit->getDisplayName()); + $this->logInfo( + pht('SKIP'), + pht( + 'No changes for commit "%s".', + $commit->getDisplayName())); } } - } private function getIdentityForCommit( @@ -113,4 +253,131 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow return $this->identityCache[$raw_identity]; } + + private function rebuildUsers($users) { + $viewer = $this->getViewer(); + + foreach ($users as $user) { + $this->logInfo( + pht('USER'), + pht( + 'Rebuilding identities for user "%s".', + $user->getMonogram())); + + $emails = id(new PhabricatorUserEmail())->loadAllWhere( + 'userPHID = %s', + $user->getPHID()); + if ($emails) { + $this->rebuildEmails(mpull($emails, 'getAddress')); + } + + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withRelatedPHIDs(array($user->getPHID())) + ->execute(); + + if (!$identities) { + $this->logWarn( + pht('NO IDENTITIES'), + pht('Found no identities directly related to user.')); + continue; + } + + $this->rebuildIdentities($identities); + } + } + + private function rebuildEmails($emails) { + $viewer = $this->getViewer(); + + foreach ($emails as $email) { + $this->logInfo( + pht('EMAIL'), + pht('Rebuilding identities for email address "%s".', $email)); + + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withEmailAddresses(array($email)) + ->execute(); + + if (!$identities) { + $this->logWarn( + pht('NO IDENTITIES'), + pht('Found no identities for email address "%s".', $email)); + continue; + } + + $this->rebuildIdentities($identities); + } + } + + private function rebuildIdentities($identities) { + $viewer = $this->getViewer(); + + foreach ($identities as $identity) { + $raw_identity = $identity->getIdentityName(); + + if (isset($this->identityCache[$raw_identity])) { + $this->logInfo( + pht('SKIP'), + pht( + 'Identity "%s" has already been rebuilt.', + $raw_identity)); + continue; + } + + $this->logInfo( + pht('IDENTITY'), + pht( + 'Rebuilding identity "%s".', + $raw_identity)); + + $old_auto = $identity->getAutomaticGuessedUserPHID(); + $old_assign = $identity->getManuallySetUserPHID(); + + $identity = id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer) + ->newUpdatedIdentity($identity); + + $this->identityCache[$raw_identity] = $identity; + + $new_auto = $identity->getAutomaticGuessedUserPHID(); + $new_assign = $identity->getManuallySetUserPHID(); + + $same_auto = ($old_auto === $new_auto); + $same_assign = ($old_assign === $new_assign); + + if ($same_auto && $same_assign) { + $this->logInfo( + pht('UNCHANGED'), + pht('No changes to identity.')); + } else { + if (!$same_auto) { + $this->logWarn( + pht('AUTOMATIC PHID'), + pht( + 'Automatic user updated from "%s" to "%s".', + $this->renderPHID($old_auto), + $this->renderPHID($new_auto))); + } + if (!$same_assign) { + $this->logWarn( + pht('ASSIGNED PHID'), + pht( + 'Assigned user updated from "%s" to "%s".', + $this->renderPHID($old_assign), + $this->renderPHID($new_assign))); + } + } + } + } + + private function renderPHID($phid) { + if ($phid == null) { + return pht('NULL'); + } else { + return $phid; + } + } + } diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index 14c2a75f80..7de97de4d6 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -11,6 +11,7 @@ final class PhabricatorRepositoryIdentityQuery private $effectivePHIDs; private $identityNameLike; private $hasEffectivePHID; + private $relatedPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -47,6 +48,11 @@ final class PhabricatorRepositoryIdentityQuery return $this; } + public function withRelatedPHIDs(array $related) { + $this->relatedPHIDs = $related; + return $this; + } + public function withHasEffectivePHID($has_effective_phid) { $this->hasEffectivePHID = $has_effective_phid; return $this; @@ -57,7 +63,7 @@ final class PhabricatorRepositoryIdentityQuery } protected function getPrimaryTableAlias() { - return 'repository_identity'; + return 'identity'; } protected function loadPage() { @@ -70,28 +76,28 @@ final class PhabricatorRepositoryIdentityQuery if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'repository_identity.id IN (%Ld)', + 'identity.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'repository_identity.phid IN (%Ls)', + 'identity.phid IN (%Ls)', $this->phids); } if ($this->assignedPHIDs !== null) { $where[] = qsprintf( $conn, - 'repository_identity.manuallySetUserPHID IN (%Ls)', + 'identity.manuallySetUserPHID IN (%Ls)', $this->assignedPHIDs); } if ($this->effectivePHIDs !== null) { $where[] = qsprintf( $conn, - 'repository_identity.currentEffectiveUserPHID IN (%Ls)', + 'identity.currentEffectiveUserPHID IN (%Ls)', $this->effectivePHIDs); } @@ -99,11 +105,11 @@ final class PhabricatorRepositoryIdentityQuery if ($this->hasEffectivePHID) { $where[] = qsprintf( $conn, - 'repository_identity.currentEffectiveUserPHID IS NOT NULL'); + 'identity.currentEffectiveUserPHID IS NOT NULL'); } else { $where[] = qsprintf( $conn, - 'repository_identity.currentEffectiveUserPHID IS NULL'); + 'identity.currentEffectiveUserPHID IS NULL'); } } @@ -115,24 +121,35 @@ final class PhabricatorRepositoryIdentityQuery $where[] = qsprintf( $conn, - 'repository_identity.identityNameHash IN (%Ls)', + 'identity.identityNameHash IN (%Ls)', $name_hashes); } if ($this->emailAddresses !== null) { $where[] = qsprintf( $conn, - 'repository_identity.emailAddress IN (%Ls)', + 'identity.emailAddress IN (%Ls)', $this->emailAddresses); } if ($this->identityNameLike != null) { $where[] = qsprintf( $conn, - 'repository_identity.identityNameRaw LIKE %~', + 'identity.identityNameRaw LIKE %~', $this->identityNameLike); } + if ($this->relatedPHIDs !== null) { + $where[] = qsprintf( + $conn, + '(identity.manuallySetUserPHID IN (%Ls) OR + identity.currentEffectiveUserPHID IN (%Ls) OR + identity.automaticGuessedUserPHID IN (%Ls))', + $this->relatedPHIDs, + $this->relatedPHIDs, + $this->relatedPHIDs); + } + return $where; } diff --git a/src/infrastructure/management/PhabricatorManagementWorkflow.php b/src/infrastructure/management/PhabricatorManagementWorkflow.php index ae33f5d6cc..b1a81201f7 100644 --- a/src/infrastructure/management/PhabricatorManagementWorkflow.php +++ b/src/infrastructure/management/PhabricatorManagementWorkflow.php @@ -67,4 +67,125 @@ abstract class PhabricatorManagementWorkflow extends PhutilArgumentWorkflow { fprintf(STDERR, '%s', $message); } + final protected function loadUsersFromArguments(array $identifiers) { + if (!$identifiers) { + return array(); + } + + $ids = array(); + $phids = array(); + $usernames = array(); + + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; + + foreach ($identifiers as $identifier) { + // If the value is a user PHID, treat as a PHID. + if (phid_get_type($identifier) === $user_type) { + $phids[$identifier] = $identifier; + continue; + } + + // If the value is "@..." and then some text, treat it as a username. + if ((strlen($identifier) > 1) && ($identifier[0] == '@')) { + $usernames[$identifier] = substr($identifier, 1); + continue; + } + + // If the value is digits, treat it as both an ID and a username. + // Entirely numeric usernames, like "1234", are valid. + if (ctype_digit($identifier)) { + $ids[$identifier] = $identifier; + $usernames[$identifier] = $identifier; + continue; + } + + // Otherwise, treat it as an unescaped username. + $usernames[$identifier] = $identifier; + } + + $viewer = $this->getViewer(); + $results = array(); + + if ($phids) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->execute(); + foreach ($users as $user) { + $phid = $user->getPHID(); + $results[$phid][] = $user; + } + } + + if ($usernames) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withUsernames($usernames) + ->execute(); + + $reverse_map = array(); + foreach ($usernames as $identifier => $username) { + $username = phutil_utf8_strtolower($username); + $reverse_map[$username][] = $identifier; + } + + foreach ($users as $user) { + $username = $user->getUsername(); + $username = phutil_utf8_strtolower($username); + + $reverse_identifiers = idx($reverse_map, $username, array()); + + if (count($reverse_identifiers) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'Multiple user identifiers (%s) correspond to the same user. '. + 'Identify each user exactly once.', + implode(', ', $reverse_identifiers))); + } + + foreach ($reverse_identifiers as $reverse_identifier) { + $results[$reverse_identifier][] = $user; + } + } + } + + if ($ids) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + foreach ($users as $user) { + $id = $user->getID(); + $results[$id][] = $user; + } + } + + $list = array(); + foreach ($identifiers as $identifier) { + $users = idx($results, $identifier, array()); + if (!$users) { + throw new PhutilArgumentUsageException( + pht( + 'No user "%s" exists. Specify users by username, ID, or PHID.', + $identifier)); + } + + if (count($users) > 1) { + // This can happen if you have a user "@25", a user with ID 25, and + // specify "--user 25". You can disambiguate this by specifying + // "--user @25". + throw new PhutilArgumentUsageException( + pht( + 'Identifier "%s" matches multiple users. Specify each user '. + 'unambiguously with "@username" or by using user PHIDs.', + $identifier)); + } + + $list[] = head($users); + } + + return $list; + } + } From d69a7360ea172d8213df842493de7a320a6afa17 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Nov 2019 10:20:01 -0800 Subject: [PATCH 47/57] Use DestructionEngine to destroy UserEmail objects Summary: Ref T13444. Prepare to hook identity updates when user email addreses are destroyed. Test Plan: - Destroyed a user with `bin/remove destroy ... --trace`, saw email deleted. - Destroyed an email from the web UI, saw email deleted. Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20912 --- src/__phutil_library_map__.php | 5 ++++- .../people/editor/PhabricatorUserEditor.php | 3 ++- src/applications/people/storage/PhabricatorUser.php | 2 +- .../people/storage/PhabricatorUserEmail.php | 13 ++++++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9d85964435..ffb824f177 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -11681,7 +11681,10 @@ phutil_register_library_map(array( 'PhabricatorUserEditEngine' => 'PhabricatorEditEngine', 'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEditorTestCase' => 'PhabricatorTestCase', - 'PhabricatorUserEmail' => 'PhabricatorUserDAO', + 'PhabricatorUserEmail' => array( + 'PhabricatorUserDAO', + 'PhabricatorDestructibleInterface', + ), 'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase', 'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType', 'PhabricatorUserFerretEngine' => 'PhabricatorFerretEngine', diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 81f427ada8..34bc1ae03f 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -241,7 +241,8 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw new Exception(pht('Email not owned by user!')); } - $email->delete(); + id(new PhabricatorDestructionEngine()) + ->destroyObject($email); $log = PhabricatorUserLog::initializeNewLog( $actor, diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index cce4ffa58a..a62aab4fc3 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1148,7 +1148,7 @@ final class PhabricatorUser 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { - $email->delete(); + $engine->destroyObject($email); } $sessions = id(new PhabricatorAuthSession())->loadAllWhere( diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php index 572c7d6e8b..f626e8f133 100644 --- a/src/applications/people/storage/PhabricatorUserEmail.php +++ b/src/applications/people/storage/PhabricatorUserEmail.php @@ -4,7 +4,9 @@ * @task restrictions Domain Restrictions * @task email Email About Email */ -final class PhabricatorUserEmail extends PhabricatorUserDAO { +final class PhabricatorUserEmail + extends PhabricatorUserDAO + implements PhabricatorDestructibleInterface { protected $userPHID; protected $address; @@ -271,4 +273,13 @@ final class PhabricatorUserEmail extends PhabricatorUserDAO { return $this; } + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + } From 89dcf9792a0ab575805941f97e0c2d88821d746e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Nov 2019 11:24:22 -0800 Subject: [PATCH 48/57] Give "PhabricatorUserEmail" a PHID Summary: Ref T13444. To interact meaningfully with "DestructionEngine", objects need a PHID. The "UserEmail" object currently does not have one (or a real "Query"). Provide basic PHID support so "DestructionEngine" can interact with the object more powerfully. Test Plan: - Ran migrations, checked data in database, saw sensible PHIDs assigned. - Added a new email address to my account, saw it get a PHID. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20913 --- .../autopatches/20191114.email.01.phid.sql | 2 + .../20191114.email.02.populate.php | 18 ++++++ src/__phutil_library_map__.php | 4 ++ .../PhabricatorPeopleUserEmailPHIDType.php | 35 ++++++++++++ .../query/PhabricatorPeopleUserEmailQuery.php | 55 +++++++++++++++++++ .../people/storage/PhabricatorUserEmail.php | 5 ++ 6 files changed, 119 insertions(+) create mode 100644 resources/sql/autopatches/20191114.email.01.phid.sql create mode 100644 resources/sql/autopatches/20191114.email.02.populate.php create mode 100644 src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php create mode 100644 src/applications/people/query/PhabricatorPeopleUserEmailQuery.php diff --git a/resources/sql/autopatches/20191114.email.01.phid.sql b/resources/sql/autopatches/20191114.email.01.phid.sql new file mode 100644 index 0000000000..3851d6e0ec --- /dev/null +++ b/resources/sql/autopatches/20191114.email.01.phid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.user_email + ADD phid VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20191114.email.02.populate.php b/resources/sql/autopatches/20191114.email.02.populate.php new file mode 100644 index 0000000000..96ef13ea58 --- /dev/null +++ b/resources/sql/autopatches/20191114.email.02.populate.php @@ -0,0 +1,18 @@ +establishConnection('w'); + +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); +foreach ($iterator as $row) { + $phid = $row['phid']; + + if (!strlen($phid)) { + queryfx( + $conn, + 'UPDATE %R SET phid = %s WHERE id = %d', + $table, + $table->generatePHID(), + $row['id']); + } +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ffb824f177..30627e8b59 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4121,6 +4121,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleTasksProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php', 'PhabricatorPeopleTestDataGenerator' => 'applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php', 'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php', + 'PhabricatorPeopleUserEmailPHIDType' => 'applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php', + 'PhabricatorPeopleUserEmailQuery' => 'applications/people/query/PhabricatorPeopleUserEmailQuery.php', 'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php', 'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php', 'PhabricatorPeopleUsernameMailEngine' => 'applications/people/mail/PhabricatorPeopleUsernameMailEngine.php', @@ -10617,6 +10619,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleTasksProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorPeopleUserEmailPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorPeopleUserEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleUsernameMailEngine' => 'PhabricatorPeopleMailEngine', diff --git a/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php new file mode 100644 index 0000000000..0aadc149f6 --- /dev/null +++ b/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php @@ -0,0 +1,35 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + return null; + } + +} diff --git a/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php new file mode 100644 index 0000000000..f0330f34f9 --- /dev/null +++ b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php @@ -0,0 +1,55 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorUserEmail(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function getPrimaryTableAlias() { + return 'email'; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'email.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'email.phid IN (%Ls)', + $this->phids); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorPeopleApplication'; + } + +} diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php index f626e8f133..cf2c61dc03 100644 --- a/src/applications/people/storage/PhabricatorUserEmail.php +++ b/src/applications/people/storage/PhabricatorUserEmail.php @@ -18,6 +18,7 @@ final class PhabricatorUserEmail protected function getConfiguration() { return array( + self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'address' => 'sort128', 'isVerified' => 'bool', @@ -36,6 +37,10 @@ final class PhabricatorUserEmail ) + parent::getConfiguration(); } + public function getPHIDType() { + return PhabricatorPeopleUserEmailPHIDType::TYPECONST; + } + public function getVerificationURI() { return '/emailverify/'.$this->getVerificationCode().'/'; } From a7aca500bcd1b46eeafd47bd21d07bc7b70746db Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Nov 2019 10:35:05 -0800 Subject: [PATCH 49/57] Update repository identities after all mutations to users and email addresses Summary: Ref T13444. Currently, many mutations to users and email addresses (particularly: user creation; and user and address destruction) do not propagate properly to repository identities. Add hooks to all mutation workflows so repository identities get rebuilt properly when users are created, email addresses are removed, users or email addresses are destroyed, or email addresses are reassigned. Test Plan: - Added random email address to account, removed it. - Added unassociated email address to account, saw identity update (and associate). - Removed it, saw identity update (and disassociate). - Registered an account with an unassociated email address, saw identity update (and associate). - Destroyed the account, saw identity update (and disassociate). - Added address X to account A, unverified. - Invited address X. - Clicked invite link as account B. - Confirmed desire to steal address. - Saw identity update and reassociate. Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20914 --- src/__phutil_library_map__.php | 2 + ...toryIdentityDestructionEngineExtension.php | 40 ++++++++++++ .../DiffusionRepositoryIdentityEngine.php | 8 +++ .../people/editor/PhabricatorUserEditor.php | 18 +++--- ...bricatorRepositoryIdentityChangeWorker.php | 62 +++++++++++++------ .../engine/PhabricatorDestructionEngine.php | 51 ++++++++++++++- .../PhabricatorDestructionEngineExtension.php | 12 +++- 7 files changed, 164 insertions(+), 29 deletions(-) create mode 100644 src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 30627e8b59..f26f36d52e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -984,6 +984,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryFunctionDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryFunctionDatasource.php', 'DiffusionRepositoryHistoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryHistoryManagementPanel.php', + 'DiffusionRepositoryIdentityDestructionEngineExtension' => 'applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php', 'DiffusionRepositoryIdentityEditor' => 'applications/diffusion/editor/DiffusionRepositoryIdentityEditor.php', 'DiffusionRepositoryIdentityEngine' => 'applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php', 'DiffusionRepositoryIdentitySearchEngine' => 'applications/diffusion/query/DiffusionRepositoryIdentitySearchEngine.php', @@ -6968,6 +6969,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryManageController', 'DiffusionRepositoryFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DiffusionRepositoryHistoryManagementPanel' => 'DiffusionRepositoryManagementPanel', + 'DiffusionRepositoryIdentityDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'DiffusionRepositoryIdentityEditor' => 'PhabricatorApplicationTransactionEditor', 'DiffusionRepositoryIdentityEngine' => 'Phobject', 'DiffusionRepositoryIdentitySearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php new file mode 100644 index 0000000000..effbe47981 --- /dev/null +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityDestructionEngineExtension.php @@ -0,0 +1,40 @@ +getPHID(); + } + + if ($object instanceof PhabricatorUserEmail) { + $email_addresses[] = $object->getAddress(); + } + + if ($related_phids || $email_addresses) { + PhabricatorWorker::scheduleTask( + 'PhabricatorRepositoryIdentityChangeWorker', + array( + 'relatedPHIDs' => $related_phids, + 'emailAddresses' => $email_addresses, + )); + } + } + +} diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php index 635622fda2..d832a1ac95 100644 --- a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php @@ -95,4 +95,12 @@ final class DiffusionRepositoryIdentityEngine return $identity; } + public function didUpdateEmailAddress($raw_address) { + PhabricatorWorker::scheduleTask( + 'PhabricatorRepositoryIdentityChangeWorker', + array( + 'emailAddresses' => array($raw_address), + )); + } + } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 34bc1ae03f..2bc7a4f1d8 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -89,6 +89,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $this->didVerifyEmail($user, $email); } + id(new DiffusionRepositoryIdentityEngine()) + ->didUpdateEmailAddress($email->getAddress()); + return $this; } @@ -202,11 +205,8 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->endWriteLocking(); $user->saveTransaction(); - // Try and match this new address against unclaimed `RepositoryIdentity`s - PhabricatorWorker::scheduleTask( - 'PhabricatorRepositoryIdentityChangeWorker', - array('userPHID' => $user->getPHID()), - array('objectPHID' => $user->getPHID())); + id(new DiffusionRepositoryIdentityEngine()) + ->didUpdateEmailAddress($email->getAddress()); return $this; } @@ -241,7 +241,8 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw new Exception(pht('Email not owned by user!')); } - id(new PhabricatorDestructionEngine()) + $destruction_engine = id(new PhabricatorDestructionEngine()) + ->setWaitToFinalizeDestruction(true) ->destroyObject($email); $log = PhabricatorUserLog::initializeNewLog( @@ -255,6 +256,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->saveTransaction(); $this->revokePasswordResetLinks($user); + $destruction_engine->finalizeDestruction(); return $this; } @@ -327,7 +329,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } $email->sendNewPrimaryEmail($user); - $this->revokePasswordResetLinks($user); return $this; @@ -441,6 +442,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $user->endWriteLocking(); $user->saveTransaction(); + + id(new DiffusionRepositoryIdentityEngine()) + ->didUpdateEmailAddress($email->getAddress()); } diff --git a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php index 3d5cac8a03..3cc86d7f92 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php @@ -6,30 +6,54 @@ final class PhabricatorRepositoryIdentityChangeWorker protected function doWork() { $viewer = PhabricatorUser::getOmnipotentUser(); - $task_data = $this->getTaskData(); - $user_phid = idx($task_data, 'userPHID'); + $related_phids = $this->getTaskDataValue('relatedPHIDs'); + $email_addresses = $this->getTaskDataValue('emailAddresses'); - $user = id(new PhabricatorPeopleQuery()) - ->withPHIDs(array($user_phid)) - ->setViewer($viewer) - ->executeOne(); + // Retain backward compatibility with older tasks which may still be in + // queue. Previously, this worker accepted a single "userPHID". See + // T13444. This can be removed in some future version of Phabricator once + // these tasks have likely flushed out of queue. + $legacy_phid = $this->getTaskDataValue('userPHID'); + if ($legacy_phid) { + if (!is_array($related_phids)) { + $related_phids = array(); + } + $related_phids[] = $legacy_phid; + } - $emails = id(new PhabricatorUserEmail())->loadAllWhere( - 'userPHID = %s', - $user->getPHID()); + // Note that we may arrive in this worker after the associated objects + // have already been destroyed, so we can't (and shouldn't) verify that + // PHIDs correspond to real objects. If you "bin/remove destroy" a user, + // we'll end up here with a now-bogus user PHID that we should + // disassociate from identities. + + $identity_map = array(); + + if ($related_phids) { + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withRelatedPHIDs($related_phids) + ->execute(); + $identity_map += mpull($identities, null, 'getPHID'); + } + + if ($email_addresses) { + $identities = id(new PhabricatorRepositoryIdentityQuery()) + ->setViewer($viewer) + ->withEmailAddresses($email_addresses) + ->execute(); + $identity_map += mpull($identities, null, 'getPHID'); + } + + // If we didn't find any related identities, we're all set. + if (!$identity_map) { + return; + } $identity_engine = id(new DiffusionRepositoryIdentityEngine()) ->setViewer($viewer); - - foreach ($emails as $email) { - $identities = id(new PhabricatorRepositoryIdentityQuery()) - ->setViewer($viewer) - ->withEmailAddresses(array($email->getAddress())) - ->execute(); - - foreach ($identities as $identity) { - $identity_engine->newUpdatedIdentity($identity); - } + foreach ($identity_map as $identity) { + $identity_engine->newUpdatedIdentity($identity); } } diff --git a/src/applications/system/engine/PhabricatorDestructionEngine.php b/src/applications/system/engine/PhabricatorDestructionEngine.php index 336df57756..9006073888 100644 --- a/src/applications/system/engine/PhabricatorDestructionEngine.php +++ b/src/applications/system/engine/PhabricatorDestructionEngine.php @@ -5,6 +5,9 @@ final class PhabricatorDestructionEngine extends Phobject { private $rootLogID; private $collectNotes; private $notes = array(); + private $depth = 0; + private $destroyedObjects = array(); + private $waitToFinalizeDestruction = false; public function setCollectNotes($collect_notes) { $this->collectNotes = $collect_notes; @@ -19,9 +22,20 @@ final class PhabricatorDestructionEngine extends Phobject { return PhabricatorUser::getOmnipotentUser(); } + public function setWaitToFinalizeDestruction($wait) { + $this->waitToFinalizeDestruction = $wait; + return $this; + } + + public function getWaitToFinalizeDestruction() { + return $this->waitToFinalizeDestruction; + } + public function destroyObject(PhabricatorDestructibleInterface $object) { + $this->depth++; + $log = id(new PhabricatorSystemDestructionLog()) - ->setEpoch(time()) + ->setEpoch(PhabricatorTime::getNow()) ->setObjectClass(get_class($object)); if ($this->rootLogID) { @@ -73,7 +87,42 @@ final class PhabricatorDestructionEngine extends Phobject { foreach ($extensions as $key => $extension) { $extension->destroyObject($this, $object); } + + $this->destroyedObjects[] = $object; } + + $this->depth--; + + // If this is a root-level invocation of "destroyObject()", flush the + // queue of destroyed objects and fire "didDestroyObject()" hooks. This + // hook allows extensions to do things like queue cache updates which + // might race if we fire them during object destruction. + + if (!$this->depth) { + if (!$this->getWaitToFinalizeDestruction()) { + $this->finalizeDestruction(); + } + } + + return $this; + } + + public function finalizeDestruction() { + $extensions = PhabricatorDestructionEngineExtension::getAllExtensions(); + + foreach ($this->destroyedObjects as $object) { + foreach ($extensions as $extension) { + if (!$extension->canDestroyObject($this, $object)) { + continue; + } + + $extension->didDestroyObject($this, $object); + } + } + + $this->destroyedObjects = array(); + + return $this; } private function getObjectPHID($object) { diff --git a/src/applications/system/engine/PhabricatorDestructionEngineExtension.php b/src/applications/system/engine/PhabricatorDestructionEngineExtension.php index 3146e5d611..17c5e9e5cb 100644 --- a/src/applications/system/engine/PhabricatorDestructionEngineExtension.php +++ b/src/applications/system/engine/PhabricatorDestructionEngineExtension.php @@ -14,9 +14,17 @@ abstract class PhabricatorDestructionEngineExtension extends Phobject { return true; } - abstract public function destroyObject( + public function destroyObject( PhabricatorDestructionEngine $engine, - $object); + $object) { + return null; + } + + public function didDestroyObject( + PhabricatorDestructionEngine $engine, + $object) { + return null; + } final public static function getAllExtensions() { return id(new PhutilClassMapQuery()) From 63d84e0b44b761e03cdbbe1615fa2cefcfb4327d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Nov 2019 10:05:00 -0800 Subject: [PATCH 50/57] Improve use of keys when iterating over commits in "bin/audit delete" and "bin/repository rebuild-identities" Summary: Fixes T13457. Ref T13444. When we iterate over commits in a particular repository, the default iteration strategy can't effectively use the keys on the table. Tweak the ordering so the "" key can be used. Test Plan: - Ran `bin/audit delete --repository X` and `bin/repository rebuild-identities --repository X` before and after changes. - With just the key changes, performance was slightly better. My local data isn't large enough to really emphasize the key changes. - With the page size changes, performance was a bit better (~30%, but on 1-3 second run durations). - Used `--trace` and ran `EXPLAIN ...` on the new queries, saw them select the "" key and report a bare "Using index condition" in the "Extra" column. Maniphest Tasks: T13457, T13444 Differential Revision: https://secure.phabricator.com/D20921 --- .../PhabricatorAuditManagementDeleteWorkflow.php | 12 +++++++++++- ...RepositoryManagementRebuildIdentitiesWorkflow.php | 7 ++++++- .../storage/lisk/PhabricatorQueryIterator.php | 7 ++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php index 7d14df7239..cd621ce821 100644 --- a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php @@ -93,6 +93,12 @@ final class PhabricatorAuditManagementDeleteWorkflow if ($repos) { $query->withRepositoryIDs(mpull($repos, 'getID')); + + // See T13457. If we're iterating over commits in a single large + // repository, the lack of a "" key can slow things + // down. Iterate in a specific order to use a key which is present + // on the table (""). + $query->setOrderVector(array('-epoch', '-id')); } $auditor_map = array(); @@ -105,7 +111,11 @@ final class PhabricatorAuditManagementDeleteWorkflow $query->withPHIDs(mpull($commits, 'getPHID')); } - $commit_iterator = new PhabricatorQueryIterator($query); + $commit_iterator = id(new PhabricatorQueryIterator($query)); + + // See T13457. We may be examining many commits; each commit is small so + // we can safely increase the page size to improve performance a bit. + $commit_iterator->setPageSize(1000); $audits = array(); foreach ($commit_iterator as $commit) { diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index c1f92e8371..513d7a3805 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -98,7 +98,12 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow ->needCommitData(true) ->withRepositoryIDs(array($repository->getID())); - $commit_iterator = new PhabricatorQueryIterator($commit_query); + // See T13457. Adjust ordering to hit keys better and tweak page size + // to improve performance slightly, since these records are small. + $commit_query->setOrderVector(array('-epoch', '-id')); + + $commit_iterator = id(new PhabricatorQueryIterator($commit_query)) + ->setPageSize(1000); $this->rebuildCommits($commit_iterator); } diff --git a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php index 648b83863a..cc88678cdf 100644 --- a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php +++ b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php @@ -10,7 +10,12 @@ final class PhabricatorQueryIterator extends PhutilBufferedIterator { } protected function didRewind() { - $this->pager = new AphrontCursorPagerView(); + $pager = new AphrontCursorPagerView(); + + $page_size = $this->getPageSize(); + $pager->setPageSize($page_size); + + $this->pager = $pager; } public function key() { From 374f8b10b3cffd35233302d439d97efe1c04e6bd Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Nov 2019 12:31:58 -0800 Subject: [PATCH 51/57] Add a "--dry-run" flag to "bin/repository rebuild-identities" Summary: Ref T13444. Allow the effects of performing an identity rebuild to be previewed without committing to any changes. Test Plan: Ran "bin/repository rebuild-identities --all-identities" with and without "--dry-run". Maniphest Tasks: T13444 Differential Revision: https://secure.phabricator.com/D20922 --- .../DiffusionRepositoryIdentityEngine.php | 20 ++++- ...oryManagementRebuildIdentitiesWorkflow.php | 89 ++++++++++++++----- 2 files changed, 84 insertions(+), 25 deletions(-) diff --git a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php index d832a1ac95..8cd8a4bfa2 100644 --- a/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php +++ b/src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php @@ -5,6 +5,7 @@ final class DiffusionRepositoryIdentityEngine private $viewer; private $sourcePHID; + private $dryRun; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -28,6 +29,15 @@ final class DiffusionRepositoryIdentityEngine return $this->sourcePHID; } + public function setDryRun($dry_run) { + $this->dryRun = $dry_run; + return $this; + } + + public function getDryRun() { + return $this->dryRun; + } + public function newResolvedIdentity($raw_identity) { $identity = $this->loadRawIdentity($raw_identity); @@ -88,9 +98,13 @@ final class DiffusionRepositoryIdentityEngine $resolved_phid = $this->resolveIdentity($identity); - $identity - ->setAutomaticGuessedUserPHID($resolved_phid) - ->save(); + $identity->setAutomaticGuessedUserPHID($resolved_phid); + + if ($this->getDryRun()) { + $identity->makeEphemeral(); + } else { + $identity->save(); + } return $identity; } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index 513d7a3805..bd906aa5da 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -4,6 +4,8 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow extends PhabricatorRepositoryManagementWorkflow { private $identityCache = array(); + private $phidCache = array(); + private $dryRun; protected function didConstruct() { $this @@ -51,6 +53,10 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow 'repeat' => true, 'help' => pht('Rebuild identities for a raw commit string.'), ), + array( + 'name' => 'dry-run', + 'help' => pht('Show changes, but do not make any changes.'), + ), )); } @@ -59,7 +65,6 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow $rebuilt_anything = false; - $all_repositories = $args->getArg('all-repositories'); $repositories = $args->getArg('repository'); @@ -81,6 +86,15 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow 'compatible.')); } + $dry_run = $args->getArg('dry-run'); + $this->dryRun = $dry_run; + + if ($this->dryRun) { + $this->logWarn( + pht('DRY RUN'), + pht('This is a dry run, so no changes will be written.')); + } + if ($all_repositories || $repositories) { $rebuilt_anything = true; @@ -245,10 +259,7 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow $raw_identity) { if (!isset($this->identityCache[$raw_identity])) { - $viewer = $this->getViewer(); - - $identity = id(new DiffusionRepositoryIdentityEngine()) - ->setViewer($viewer) + $identity = $this->newIdentityEngine() ->setSourcePHID($commit->getPHID()) ->newResolvedIdentity($raw_identity); @@ -317,7 +328,7 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow } private function rebuildIdentities($identities) { - $viewer = $this->getViewer(); + $dry_run = $this->dryRun; foreach ($identities as $identity) { $raw_identity = $identity->getIdentityName(); @@ -340,8 +351,7 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow $old_auto = $identity->getAutomaticGuessedUserPHID(); $old_assign = $identity->getManuallySetUserPHID(); - $identity = id(new DiffusionRepositoryIdentityEngine()) - ->setViewer($viewer) + $identity = $this->newIdentityEngine() ->newUpdatedIdentity($identity); $this->identityCache[$raw_identity] = $identity; @@ -358,20 +368,38 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow pht('No changes to identity.')); } else { if (!$same_auto) { - $this->logWarn( - pht('AUTOMATIC PHID'), - pht( - 'Automatic user updated from "%s" to "%s".', - $this->renderPHID($old_auto), - $this->renderPHID($new_auto))); + if ($dry_run) { + $this->logWarn( + pht('DETECTED PHID'), + pht( + '(Dry Run) Would update detected user from "%s" to "%s".', + $this->renderPHID($old_auto), + $this->renderPHID($new_auto))); + } else { + $this->logWarn( + pht('DETECTED PHID'), + pht( + 'Detected user updated from "%s" to "%s".', + $this->renderPHID($old_auto), + $this->renderPHID($new_auto))); + } } if (!$same_assign) { - $this->logWarn( - pht('ASSIGNED PHID'), - pht( - 'Assigned user updated from "%s" to "%s".', - $this->renderPHID($old_assign), - $this->renderPHID($new_assign))); + if ($dry_run) { + $this->logWarn( + pht('ASSIGNED PHID'), + pht( + '(Dry Run) Would update assigned user from "%s" to "%s".', + $this->renderPHID($old_assign), + $this->renderPHID($new_assign))); + } else { + $this->logWarn( + pht('ASSIGNED PHID'), + pht( + 'Assigned user updated from "%s" to "%s".', + $this->renderPHID($old_assign), + $this->renderPHID($new_assign))); + } } } } @@ -380,9 +408,26 @@ final class PhabricatorRepositoryManagementRebuildIdentitiesWorkflow private function renderPHID($phid) { if ($phid == null) { return pht('NULL'); - } else { - return $phid; } + + if (!isset($this->phidCache[$phid])) { + $viewer = $this->getViewer(); + $handles = $viewer->loadHandles(array($phid)); + $this->phidCache[$phid] = pht( + '%s <%s>', + $handles[$phid]->getFullName(), + $phid); + } + + return $this->phidCache[$phid]; + } + + private function newIdentityEngine() { + $viewer = $this->getViewer(); + + return id(new DiffusionRepositoryIdentityEngine()) + ->setViewer($viewer) + ->setDryRun($this->dryRun); } } From 2abf292821a2b3938dfbf20f306e6d30b79ac1fa Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Nov 2019 11:22:45 -0800 Subject: [PATCH 52/57] Fix an issue where editing paths in Owners packages could raise an error: undefined index "display" Summary: Fixes T13464. Fully-realized paths have a "path" (normalized, effective path) and a "display" path (user-facing, un-normalized path). During transaction validation we build ref keys for paths before we normalize the "display" values. A few different approaches could be taken to resolve this, but just default the "display" path to the raw "path" if it isn't present since that seems simplest. Test Plan: Edited paths in an Owners package, no longer saw a warning in the logs. Maniphest Tasks: T13464 Differential Revision: https://secure.phabricator.com/D20923 --- .../owners/storage/PhabricatorOwnersPath.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php index 5bb32f8407..4e85ffd1b9 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPath.php +++ b/src/applications/owners/storage/PhabricatorOwnersPath.php @@ -91,11 +91,22 @@ final class PhabricatorOwnersPath extends PhabricatorOwnersDAO { } private static function getScalarKeyForRef(array $ref) { + // See T13464. When building refs from raw transactions, the path has + // not been normalized yet and doesn't have a separate "display" path. + // If the "display" path isn't populated, just use the actual path to + // build the ref key. + + if (isset($ref['display'])) { + $display = $ref['display']; + } else { + $display = $ref['path']; + } + return sprintf( 'repository=%s path=%s display=%s excluded=%d', $ref['repositoryPHID'], $ref['path'], - $ref['display'], + $display, $ref['excluded']); } From eb6df7a2091a9477523c97b485277ab68af61a0a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Nov 2019 11:53:06 -0800 Subject: [PATCH 53/57] Remove "phlog()" of exeptions during Conduit calls Summary: Fixes T13465. This "phlog()" made some degree of sense at one time, but is no longer useful or consistent. Get rid of it. See T13465 for discussion. Test Plan: Made a conduit call that hit a policy error, no longer saw error in log. Maniphest Tasks: T13465 Differential Revision: https://secure.phabricator.com/D20924 --- .../conduit/controller/PhabricatorConduitAPIController.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index cab9276674..d34189125a 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -99,9 +99,6 @@ final class PhabricatorConduitAPIController list($error_code, $error_info) = $auth_error; } } catch (Exception $ex) { - if (!($ex instanceof ConduitMethodNotFoundException)) { - phlog($ex); - } $result = null; $error_code = ($ex instanceof ConduitException ? 'ERR-CONDUIT-CALL' From 1667acfa5d7148b7e465649ddbd17680d0911a66 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Nov 2019 15:01:45 -0800 Subject: [PATCH 54/57] Implement "PolicyInterface" on "UserEmail" so "EmailQuery" can load them properly Summary: See PHI1558. Ref T11860. Ref T13444. I partly implemented PHIDs for "UserEmail" objects, but they can't load on their own so you can't directly `bin/remove destroy` them yet. Allow them to actually load by implementing "PolicyInterface". Addresses are viewable and editable by the associated user, unless they are a bot/list address, in which case they are viewable and editable by administrators (in preparation for T11860). This has no real impact on anything today. Test Plan: - Used `bin/remove destroy ` to destroy an individual email address. - Before: error while loading the object by PHID in the query policy layer. - After: clean load and destroy. Maniphest Tasks: T13444, T11860 Differential Revision: https://secure.phabricator.com/D20927 --- src/__phutil_library_map__.php | 1 + .../PhabricatorPeopleUserEmailPHIDType.php | 6 +++ .../query/PhabricatorPeopleUserEmailQuery.php | 26 +++++++++++++ .../people/storage/PhabricatorUserEmail.php | 39 ++++++++++++++++++- 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f26f36d52e..174c0e4876 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -11690,6 +11690,7 @@ phutil_register_library_map(array( 'PhabricatorUserEmail' => array( 'PhabricatorUserDAO', 'PhabricatorDestructibleInterface', + 'PhabricatorPolicyInterface', ), 'PhabricatorUserEmailTestCase' => 'PhabricatorTestCase', 'PhabricatorUserEmpowerTransaction' => 'PhabricatorUserTransactionType', diff --git a/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php b/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php index 0aadc149f6..e0f0478033 100644 --- a/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php +++ b/src/applications/people/phid/PhabricatorPeopleUserEmailPHIDType.php @@ -29,6 +29,12 @@ final class PhabricatorPeopleUserEmailPHIDType PhabricatorHandleQuery $query, array $handles, array $objects) { + + foreach ($handles as $phid => $handle) { + $email = $objects[$phid]; + $handle->setName($email->getAddress()); + } + return null; } diff --git a/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php index f0330f34f9..6e2627a96d 100644 --- a/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php +++ b/src/applications/people/query/PhabricatorPeopleUserEmailQuery.php @@ -48,6 +48,32 @@ final class PhabricatorPeopleUserEmailQuery return $where; } + protected function willLoadPage(array $page) { + + $user_phids = mpull($page, 'getUserPHID'); + + $users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($user_phids) + ->execute(); + $users = mpull($users, null, 'getPHID'); + + foreach ($page as $key => $address) { + $user = idx($users, $address->getUserPHID()); + + if (!$user) { + unset($page[$key]); + $this->didRejectResult($address); + continue; + } + + $address->attachUser($user); + } + + return $page; + } + public function getQueryApplicationClass() { return 'PhabricatorPeopleApplication'; } diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php index cf2c61dc03..4e43b2fb41 100644 --- a/src/applications/people/storage/PhabricatorUserEmail.php +++ b/src/applications/people/storage/PhabricatorUserEmail.php @@ -6,7 +6,9 @@ */ final class PhabricatorUserEmail extends PhabricatorUserDAO - implements PhabricatorDestructibleInterface { + implements + PhabricatorDestructibleInterface, + PhabricatorPolicyInterface { protected $userPHID; protected $address; @@ -14,6 +16,8 @@ final class PhabricatorUserEmail protected $isPrimary; protected $verificationCode; + private $user = self::ATTACHABLE; + const MAX_ADDRESS_LENGTH = 128; protected function getConfiguration() { @@ -52,6 +56,15 @@ final class PhabricatorUserEmail return parent::save(); } + public function attachUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function getUser() { + return $this->assertAttached($this->user); + } + /* -( Domain Restrictions )------------------------------------------------ */ @@ -287,4 +300,28 @@ final class PhabricatorUserEmail $this->delete(); } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + $user = $this->getUser(); + + if ($this->getIsSystemAgent() || $this->getIsMailingList()) { + return PhabricatorPolicies::POLICY_ADMIN; + } + + return $user->getPHID(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + } From 33c534f9b74f5aa8c9491c875292ca31a4bdc84f Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Nov 2019 14:34:32 -0800 Subject: [PATCH 55/57] Extend Config to full-width Summary: Ref T13362. Some applications moved to fixed-width a while ago but I was generally unsatisfied with where they ended up and have been pushing them back to full-width. Push Config back to full-width. Some of the subpages end up a little weird, but this provides more space to work with to make some improvements, like makign `maniphest.statuses` more legible in the UI> Test Plan: Grepped for `setFixed(`, updated each page in `/config/`. Browsed each controller, saw workable full-width UIs. Maniphest Tasks: T13362 Differential Revision: https://secure.phabricator.com/D20925 --- .../controller/PhabricatorConfigAllController.php | 5 ++--- .../PhabricatorConfigApplicationController.php | 5 ++--- .../controller/PhabricatorConfigCacheController.php | 5 ++--- .../PhabricatorConfigClusterDatabasesController.php | 5 ++--- ...abricatorConfigClusterNotificationsController.php | 5 ++--- ...habricatorConfigClusterRepositoriesController.php | 12 ++++++------ .../PhabricatorConfigClusterSearchController.php | 5 ++--- .../controller/PhabricatorConfigController.php | 2 -- .../PhabricatorConfigDatabaseIssueController.php | 5 ++--- .../PhabricatorConfigDatabaseStatusController.php | 5 ++--- .../controller/PhabricatorConfigEditController.php | 6 +++--- .../controller/PhabricatorConfigGroupController.php | 5 ++--- .../PhabricatorConfigHistoryController.php | 5 ++--- .../PhabricatorConfigIssueListController.php | 5 ++--- .../PhabricatorConfigIssueViewController.php | 5 ++--- .../controller/PhabricatorConfigListController.php | 5 ++--- .../controller/PhabricatorConfigModuleController.php | 5 ++--- .../PhabricatorConfigVersionController.php | 6 ++---- 18 files changed, 39 insertions(+), 57 deletions(-) diff --git a/src/applications/config/controller/PhabricatorConfigAllController.php b/src/applications/config/controller/PhabricatorConfigAllController.php index 3a19eff006..7452b29e21 100644 --- a/src/applications/config/controller/PhabricatorConfigAllController.php +++ b/src/applications/config/controller/PhabricatorConfigAllController.php @@ -64,13 +64,12 @@ final class PhabricatorConfigAllController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigApplicationController.php b/src/applications/config/controller/PhabricatorConfigApplicationController.php index b4f60982e7..a6b8cd38c6 100644 --- a/src/applications/config/controller/PhabricatorConfigApplicationController.php +++ b/src/applications/config/controller/PhabricatorConfigApplicationController.php @@ -18,9 +18,7 @@ final class PhabricatorConfigApplicationController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($apps_list); + ->setFooter($apps_list); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($title) @@ -29,6 +27,7 @@ final class PhabricatorConfigApplicationController return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigCacheController.php b/src/applications/config/controller/PhabricatorConfigCacheController.php index a23ab1f9c1..36642657c9 100644 --- a/src/applications/config/controller/PhabricatorConfigCacheController.php +++ b/src/applications/config/controller/PhabricatorConfigCacheController.php @@ -33,13 +33,12 @@ final class PhabricatorConfigCacheController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($page); + ->setFooter($page); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index 43e5a15b9d..417fa9d3a1 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -26,13 +26,12 @@ final class PhabricatorConfigClusterDatabasesController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($status); + ->setFooter($status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php index 443b51a903..e9f64d411a 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php @@ -28,13 +28,12 @@ final class PhabricatorConfigClusterNotificationsController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($status); + ->setFooter($status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php b/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php index 471c4cedf0..eb83a28a2a 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterRepositoriesController.php @@ -32,16 +32,16 @@ final class PhabricatorConfigClusterRepositoriesController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn(array( - $repo_status, - $repo_errors, - )); + ->setFooter( + array( + $repo_status, + $repo_errors, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterSearchController.php b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php index cd00ef73a0..55caeb1cad 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterSearchController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterSearchController.php @@ -26,13 +26,12 @@ final class PhabricatorConfigClusterSearchController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($search_status); + ->setFooter($search_status); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php index ad5ea6cd27..1b6a5af8b5 100644 --- a/src/applications/config/controller/PhabricatorConfigController.php +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -7,8 +7,6 @@ abstract class PhabricatorConfigController extends PhabricatorController { } public function buildSideNavView($filter = null, $for_app = false) { - - $guide_href = new PhutilURI('/guides/'); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index 708a708043..fa6bcab5e6 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -167,13 +167,12 @@ final class PhabricatorConfigDatabaseIssueController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php index 760317ae80..6831a048d5 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php @@ -142,13 +142,12 @@ final class PhabricatorConfigDatabaseStatusController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($body); + ->setFooter($body); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php index 224705e181..381b54e046 100644 --- a/src/applications/config/controller/PhabricatorConfigEditController.php +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -237,9 +237,8 @@ final class PhabricatorConfigEditController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn(array( + ->setFooter( + array( $error_view, $form_box, $status_items, @@ -250,6 +249,7 @@ final class PhabricatorConfigEditController return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($view); } diff --git a/src/applications/config/controller/PhabricatorConfigGroupController.php b/src/applications/config/controller/PhabricatorConfigGroupController.php index 7a3f77dfea..f981c1c1a1 100644 --- a/src/applications/config/controller/PhabricatorConfigGroupController.php +++ b/src/applications/config/controller/PhabricatorConfigGroupController.php @@ -36,13 +36,12 @@ final class PhabricatorConfigGroupController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigHistoryController.php b/src/applications/config/controller/PhabricatorConfigHistoryController.php index 9157ecb8bb..495102b6a2 100644 --- a/src/applications/config/controller/PhabricatorConfigHistoryController.php +++ b/src/applications/config/controller/PhabricatorConfigHistoryController.php @@ -36,13 +36,12 @@ final class PhabricatorConfigHistoryController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($timeline); + ->setFooter($timeline); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigIssueListController.php b/src/applications/config/controller/PhabricatorConfigIssueListController.php index 0ca94abe04..6518ccec97 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueListController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueListController.php @@ -59,13 +59,12 @@ final class PhabricatorConfigIssueListController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($issue_list); + ->setFooter($issue_list); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigIssueViewController.php b/src/applications/config/controller/PhabricatorConfigIssueViewController.php index 29c9078413..2967169e38 100644 --- a/src/applications/config/controller/PhabricatorConfigIssueViewController.php +++ b/src/applications/config/controller/PhabricatorConfigIssueViewController.php @@ -47,13 +47,12 @@ final class PhabricatorConfigIssueViewController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($content); + ->setFooter($content); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php index 1a136ea416..38a0afc328 100644 --- a/src/applications/config/controller/PhabricatorConfigListController.php +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -22,13 +22,12 @@ final class PhabricatorConfigListController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($core_list); + ->setFooter($core_list); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigModuleController.php b/src/applications/config/controller/PhabricatorConfigModuleController.php index 63cc5b3843..fe919c57e4 100644 --- a/src/applications/config/controller/PhabricatorConfigModuleController.php +++ b/src/applications/config/controller/PhabricatorConfigModuleController.php @@ -28,13 +28,12 @@ final class PhabricatorConfigModuleController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); } diff --git a/src/applications/config/controller/PhabricatorConfigVersionController.php b/src/applications/config/controller/PhabricatorConfigVersionController.php index a9571a1f85..153d363062 100644 --- a/src/applications/config/controller/PhabricatorConfigVersionController.php +++ b/src/applications/config/controller/PhabricatorConfigVersionController.php @@ -23,15 +23,13 @@ final class PhabricatorConfigVersionController $content = id(new PHUITwoColumnView()) ->setHeader($header) - ->setNavigation($nav) - ->setFixed(true) - ->setMainColumn($view); + ->setFooter($view); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setNavigation($nav) ->appendChild($content); - } public function renderModuleStatus($viewer) { From 4cd333b33fb1495c2b5a0b2ea4123110d1533249 Mon Sep 17 00:00:00 2001 From: Arturas Moskvinas Date: Mon, 9 Dec 2019 13:02:14 +0200 Subject: [PATCH 56/57] Use same method to get object URI as used in DifferentialTransactionEditor and PhabricatorApplicationTransactionEditor Summary: Maniphest object has `getURI` method, let's use it Test Plan: Create event in task - URI generated as expected in email notification Reviewers: epriestley, Pawka, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20935 --- .../maniphest/editor/ManiphestTransactionEditor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index ed98ad8ad8..01fc0af83d 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -226,7 +226,7 @@ final class ManiphestTransactionEditor $body->addLinkSection( pht('TASK DETAIL'), - PhabricatorEnv::getProductionURI('/T'.$object->getID())); + $this->getObjectLinkButtonURIForMail($object)); $board_phids = array(); From 54bcbdaba94a3573e128c6498816dbfa41d3a9cb Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 13 Dec 2019 10:31:44 -0800 Subject: [PATCH 57/57] Fix an XSS issue with certain high-priority remarkup rules embedded inside lower-priority link rules Summary: See . The link rules don't test that their parameters are flat text before using them in unsafe contexts. Since almost all rules are lower-priority than these link rules, this behavior isn't obvious. However, two rules have broadly higher priority (monospaced text, and one variation of link rules has higher priority than the other), and the latter can be used to perform an XSS attack with input in the general form `()[ [[ ... | ... ]] ]` so that the inner link rule is evaluated first, then the outer link rule uses non-flat text in an unsafe way. Test Plan: Tested examples in HackerOne report. A simple example of broken (but not unsafe) behavior is: ``` [[ `x` | `y` ]] ``` Differential Revision: https://secure.phabricator.com/D20937 --- .../markup/PhrictionRemarkupRule.php | 22 ++++++++++++++----- .../PhutilRemarkupDocumentLinkRule.php | 8 +++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/applications/phriction/markup/PhrictionRemarkupRule.php b/src/applications/phriction/markup/PhrictionRemarkupRule.php index d17de331e5..11994e0aa5 100644 --- a/src/applications/phriction/markup/PhrictionRemarkupRule.php +++ b/src/applications/phriction/markup/PhrictionRemarkupRule.php @@ -16,8 +16,23 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule { } public function markupDocumentLink(array $matches) { + $name = trim(idx($matches, 2, '')); + if (empty($matches[2])) { + $name = null; + } + + $path = trim($matches[1]); + + if (!$this->isFlatText($name)) { + return $matches[0]; + } + + if (!$this->isFlatText($path)) { + return $matches[0]; + } + // If the link contains an anchor, separate that off first. - $parts = explode('#', trim($matches[1]), 2); + $parts = explode('#', $path, 2); if (count($parts) == 2) { $link = $parts[0]; $anchor = $parts[1]; @@ -48,11 +63,6 @@ final class PhrictionRemarkupRule extends PhutilRemarkupRule { } } - $name = trim(idx($matches, 2, '')); - if (empty($matches[2])) { - $name = null; - } - // Link is now used for slug detection, so append a slash if one // is needed. $link = rtrim($link, '/').'/'; diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php index a6effa00ac..2170d9ae5e 100644 --- a/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php +++ b/src/infrastructure/markup/markuprule/PhutilRemarkupDocumentLinkRule.php @@ -136,6 +136,14 @@ final class PhutilRemarkupDocumentLinkRule extends PhutilRemarkupRule { $uri = trim($matches[1]); $name = trim(idx($matches, 2)); + if (!$this->isFlatText($uri)) { + return $matches[0]; + } + + if (!$this->isFlatText($name)) { + return $matches[0]; + } + // If whatever is being linked to begins with "/" or "#", or has "://", // or is "mailto:" or "tel:", treat it as a URI instead of a wiki page. $is_uri = preg_match('@(^/)|(://)|(^#)|(^(?:mailto|tel):)@', $uri);