From 0e2cb6e7c437d76b97ca37e554f40637228439d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 08:25:58 -0700 Subject: [PATCH 01/54] Fix missing URI for "OAuthServerClient" object handles, causing dialog with no button Summary: Ref T13330. Handles for "OAuthServerClient" objects currently do not have a URI, which causes some obscure fallout like a missing "Close" button when examining their transactions. Add a URI. Test Plan: - Viewed an OAuth server client detail page. - Edited a policy, changing it to a custom policy. - Clicked "Custom Policy" in the resulting transaction to view a dialog explaining the changes. - Before change: dialog has no close button. - After change: dialog has a close button. {F6534121} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13330 Differential Revision: https://secure.phabricator.com/D20624 --- .../phid/PhabricatorOAuthServerClientPHIDType.php | 4 +++- .../oauthserver/storage/PhabricatorOAuthServerClient.php | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php b/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php index a4d8834b96..4d3d64738b 100644 --- a/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php +++ b/src/applications/oauthserver/phid/PhabricatorOAuthServerClientPHIDType.php @@ -32,7 +32,9 @@ final class PhabricatorOAuthServerClientPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $client = $objects[$phid]; - $handle->setName($client->getName()); + $handle + ->setName($client->getName()) + ->setURI($client->getURI()); } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php index a951bf5781..471433ad4b 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerClient.php @@ -59,6 +59,12 @@ final class PhabricatorOAuthServerClient PhabricatorOAuthServerClientPHIDType::TYPECONST); } + public function getURI() { + return urisprintf( + '/oauthserver/client/view/%d/', + $this->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ From d22f6d219b0bde9e491c316be364c92b8da3b8c1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 08:35:01 -0700 Subject: [PATCH 02/54] Lightly modernize OAuth server application view pages Summary: Depends on D20624. Fixes T13330. The OAuth client pages are using some out-of-date rendering conventions; update them to modern conventions. Test Plan: Viewed a page, saw a modern header layout + curtain: {F6534135} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13330 Differential Revision: https://secure.phabricator.com/D20625 --- .../PhabricatorOAuthClientViewController.php | 93 ++++++++++--------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php index 394ace52a4..6492f78f5a 100644 --- a/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php +++ b/src/applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php @@ -15,12 +15,11 @@ final class PhabricatorOAuthClientViewController } $header = $this->buildHeaderView($client); - $actions = $this->buildActionView($client); $properties = $this->buildPropertyListView($client); - $properties->setActionList($actions); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($client->getName()); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($client->getName()) + ->setBorder(true); $timeline = $this->buildTransactionTimeline( $client, @@ -28,19 +27,27 @@ final class PhabricatorOAuthClientViewController $timeline->setShouldTerminate(true); $box = id(new PHUIObjectBoxView()) - ->setHeader($header) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); $title = pht('OAuth Application: %s', $client->getName()); - return $this->newPage() - ->setCrumbs($crumbs) - ->setTitle($title) - ->appendChild( + $curtain = $this->buildCurtain($client); + + $columns = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( array( $box, $timeline, )); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle($title) + ->appendChild($columns); } private function buildHeaderView(PhabricatorOAuthServerClient $client) { @@ -60,8 +67,9 @@ final class PhabricatorOAuthClientViewController return $header; } - private function buildActionView(PhabricatorOAuthServerClient $client) { + private function buildCurtain(PhabricatorOAuthServerClient $client) { $viewer = $this->getViewer(); + $actions = array(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -70,24 +78,19 @@ final class PhabricatorOAuthClientViewController $id = $client->getID(); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer); + $actions[] = id(new PhabricatorActionView()) + ->setName(pht('Edit Application')) + ->setIcon('fa-pencil') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit) + ->setHref($client->getEditURI()); - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Application')) - ->setIcon('fa-pencil') - ->setWorkflow(!$can_edit) - ->setDisabled(!$can_edit) - ->setHref($client->getEditURI())); - - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Show Application Secret')) - ->setIcon('fa-eye') - ->setHref($this->getApplicationURI("client/secret/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(true)); + $actions[] = id(new PhabricatorActionView()) + ->setName(pht('Show Application Secret')) + ->setIcon('fa-eye') + ->setHref($this->getApplicationURI("client/secret/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true); $is_disabled = $client->getIsDisabled(); if ($is_disabled) { @@ -100,22 +103,26 @@ final class PhabricatorOAuthClientViewController $disable_uri = $this->getApplicationURI("client/disable/{$id}/"); - $view->addAction( - id(new PhabricatorActionView()) - ->setName($disable_text) - ->setIcon($disable_icon) - ->setWorkflow(true) - ->setDisabled(!$can_edit) - ->setHref($disable_uri)); + $actions[] = id(new PhabricatorActionView()) + ->setName($disable_text) + ->setIcon($disable_icon) + ->setWorkflow(true) + ->setDisabled(!$can_edit) + ->setHref($disable_uri); - $view->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Generate Test Token')) - ->setIcon('fa-plus') - ->setWorkflow(true) - ->setHref($this->getApplicationURI("client/test/{$id}/"))); + $actions[] = id(new PhabricatorActionView()) + ->setName(pht('Generate Test Token')) + ->setIcon('fa-plus') + ->setWorkflow(true) + ->setHref($this->getApplicationURI("client/test/{$id}/")); - return $view; + $curtain = $this->newCurtainView($client); + + foreach ($actions as $action) { + $curtain->addAction($action); + } + + return $curtain; } private function buildPropertyListView(PhabricatorOAuthServerClient $client) { @@ -132,10 +139,6 @@ final class PhabricatorOAuthClientViewController pht('Redirect URI'), $client->getRedirectURI()); - $view->addProperty( - pht('Created'), - phabricator_datetime($client->getDateCreated(), $viewer)); - return $view; } } From df29b82ad6012b43d95969e9dd5635a552daab49 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 12:54:10 -0700 Subject: [PATCH 03/54] Remove unused property "slug" from Workboard View controller Summary: Ref T4900. The Workboard view controller currently has a lot of different responsibilities (it's ~1,500 lines long) because it has to manage the board filter/sort state. I'd like to split it up and make it easier to move some workboard features (like "move all tasks in column...") to other Controllers, so we can have smaller controllers implementing specific workflows. I think the state handling isn't really all that bad, it just needs to be separated a little better than it currently is. To start with, remove the unused "slug" property. Test Plan: Searched for "slug", got no hits. This class is final and the property is private, so this is certainly unused. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20626 --- .../project/controller/PhabricatorProjectBoardViewController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 8ceb576b6f..7d4d47e61b 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -6,7 +6,6 @@ final class PhabricatorProjectBoardViewController const BATCH_EDIT_ALL = 'all'; private $id; - private $slug; private $queryKey; private $sortKey; private $showHidden; From 0ae9e2c75d1f65ebcb20449051540fc8278e2ec9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 14:16:09 -0700 Subject: [PATCH 04/54] Remove property "id" from Workboard View controller Summary: Depends on D20626. Ref T4900. On this controller, "id" is a separate property, but serves little purpose and complicates separating state management. Remove it. Test Plan: Bulk edited a column, managed filters, did show/hide on columns, edited a column. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20627 --- .../PhabricatorProjectBoardViewController.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 7d4d47e61b..7d5c28d884 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -5,7 +5,6 @@ final class PhabricatorProjectBoardViewController const BATCH_EDIT_ALL = 'all'; - private $id; private $queryKey; private $sortKey; private $showHidden; @@ -284,7 +283,7 @@ final class PhabricatorProjectBoardViewController $query_key = $saved_query->getQueryKey(); $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); - $bulk_uri->replaceQueryParam('board', $this->id); + $bulk_uri->replaceQueryParam('board', $project->getID()); return id(new AphrontRedirectResponse()) ->setURI($bulk_uri); @@ -781,7 +780,6 @@ final class PhabricatorProjectBoardViewController $project = $this->getProject(); $this->showHidden = $request->getBool('hidden'); - $this->id = $project->getID(); $sort_key = $this->getDefaultSort($project); @@ -931,7 +929,7 @@ final class PhabricatorProjectBoardViewController if ($is_custom) { $uri = $this->getApplicationURI( - 'board/'.$this->id.'/filter/query/'.$key.'/'); + 'board/'.$project->getID().'/filter/query/'.$key.'/'); $item->setWorkflow(true); } else { $uri = $engine->getQueryResultsPageURI($key); @@ -1179,7 +1177,7 @@ final class PhabricatorProjectBoardViewController ->setIcon('fa-search') ->setHref($query_uri); - $edit_uri = 'board/'.$this->id.'/edit/'.$column->getID().'/'; + $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/'; $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Edit Column')) ->setIcon('fa-pencil') @@ -1188,7 +1186,7 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); $can_hide = ($can_edit && !$column->isDefaultColumn()); - $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; + $hide_uri = 'board/'.$project->getID().'/hide/'.$column->getID().'/'; $hide_uri = $this->getApplicationURI($hide_uri); $hide_uri = $this->getURIWithState($hide_uri); From 9c190d68ed3762d992ee1f86c896817538649290 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 14:48:53 -0700 Subject: [PATCH 05/54] Separate workboard view state (ordering, filtering, hidden columns) from the View controller Summary: Depends on D20627. Ref T4900. If a user orders a board by "Sort by Title", then toggles the visibility of hidden columns, we want to keep the board sorted by title. To accomplish this, we pass the board state around to all the workflows here. Pull the "bag of state properties" code out of the View controller. This class basically: - reads state from a request (order, hidden, filter); - manages defaults; - provides the application with the current settings; and - generates URIs with "?order=X&hidden=Y&filter=Z" to preserve state. This is still a little questionable/transitional since some of the controllers need more cleanup. Test Plan: Toggled state, order, filters, clicked around various workflows and saw the filters preserved. A lot of these workflows are pretty serious edge cases. For example, here's a feature this implements: - Changed workboard order to "Title". - Selected "Bulk Edit Tasks..." in an empty column and command-clicked it to open the link in a new window. - Hovered over "Cancel". - Saw the link properly generate with "?order=title", preserving the order. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20628 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectBoardController.php | 23 ++- .../PhabricatorProjectBoardViewController.php | 130 +++------------ .../state/PhabricatorWorkboardViewState.php | 150 ++++++++++++++++++ 4 files changed, 197 insertions(+), 108 deletions(-) create mode 100644 src/applications/project/state/PhabricatorWorkboardViewState.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ab5a58dda6..7f0ef2afde 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4949,6 +4949,7 @@ phutil_register_library_map(array( 'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php', 'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', + 'PhabricatorWorkboardViewState' => 'applications/project/state/PhabricatorWorkboardViewState.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', 'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php', @@ -11356,6 +11357,7 @@ phutil_register_library_map(array( 'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting', 'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType', 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', + 'PhabricatorWorkboardViewState' => 'Phobject', 'PhabricatorWorker' => 'Phobject', 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery', diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index b889bc75da..394f8a2fb8 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -1,4 +1,25 @@ viewState === null) { + $this->viewState = $this->newViewState(); + } + + return $this->viewState; + } + + final private function newViewState() { + $project = $this->getProject(); + $request = $this->getRequest(); + + return id(new PhabricatorWorkboardViewState()) + ->setProject($project) + ->readFromRequest($request); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 7d5c28d884..f06fd9c52d 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -5,10 +5,6 @@ final class PhabricatorProjectBoardViewController const BATCH_EDIT_ALL = 'all'; - private $queryKey; - private $sortKey; - private $showHidden; - public function shouldAllowPublic() { return true; } @@ -22,10 +18,8 @@ final class PhabricatorProjectBoardViewController } $project = $this->getProject(); - - $this->readRequestState(); - - $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); + $state = $this->getViewState(); + $board_uri = $project->getWorkboardURI(); $search_engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer) @@ -51,24 +45,15 @@ final class PhabricatorProjectBoardViewController ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } - return id(new AphrontRedirectResponse())->setURI( - $this->getURIWithState( - $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); + + $query_key = $saved->getQueryKey(); + $results_uri = $search_engine->getQueryResultsPageURI($query_key); + $results_uri = $state->newURI($results_uri); + + return id(new AphrontRedirectResponse())->setURI($results_uri); } - $query_key = $this->getDefaultFilter($project); - - $request_query = $request->getStr('filter'); - if (strlen($request_query)) { - $query_key = $request_query; - } - - $uri_query = $request->getURIData('queryKey'); - if (strlen($uri_query)) { - $query_key = $uri_query; - } - - $this->queryKey = $query_key; + $query_key = $state->getQueryKey(); $custom_query = null; if ($search_engine->isBuiltinQuery($query_key)) { @@ -260,7 +245,7 @@ final class PhabricatorProjectBoardViewController } if (!$batch_tasks) { - $cancel_uri = $this->getURIWithState($board_uri); + $cancel_uri = $state->newWorkboardURI(); return $this->newDialog() ->setTitle(pht('No Editable Tasks')) ->appendParagraph( @@ -308,7 +293,7 @@ final class PhabricatorProjectBoardViewController } $move_tasks = array_select_keys($tasks, $move_task_phids); - $cancel_uri = $this->getURIWithState($board_uri); + $cancel_uri = $state->newWorkboardURI(); if (!$move_tasks) { return $this->newDialog() @@ -504,7 +489,7 @@ final class PhabricatorProjectBoardViewController $column_phids = array(); $visible_phids = array(); foreach ($columns as $column) { - if (!$this->showHidden) { + if (!$state->getShowHidden()) { if ($column->isHidden()) { continue; } @@ -649,7 +634,7 @@ final class PhabricatorProjectBoardViewController ); } - $order_key = $this->sortKey; + $order_key = $state->getOrder(); $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders(); $ordering = id(clone $ordering_map[$order_key]) @@ -683,7 +668,7 @@ final class PhabricatorProjectBoardViewController 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), 'boardPHID' => $project->getPHID(), - 'order' => $this->sortKey, + 'order' => $state->getOrder(), 'orders' => $order_maps, 'headers' => $headers, 'headerKeys' => $header_keys, @@ -701,7 +686,7 @@ final class PhabricatorProjectBoardViewController $sort_menu = $this->buildSortMenu( $viewer, $project, - $this->sortKey, + $state->getOrder(), $ordering_map); $filter_menu = $this->buildFilterMenu( @@ -711,7 +696,7 @@ final class PhabricatorProjectBoardViewController $search_engine, $query_key); - $manage_menu = $this->buildManageMenu($project, $this->showHidden); + $manage_menu = $this->buildManageMenu($project, $state->getShowHidden()); $header_link = phutil_tag( 'a', @@ -775,54 +760,14 @@ final class PhabricatorProjectBoardViewController return $page; } - private function readRequestState() { - $request = $this->getRequest(); - $project = $this->getProject(); - - $this->showHidden = $request->getBool('hidden'); - - $sort_key = $this->getDefaultSort($project); - - $request_sort = $request->getStr('order'); - if ($this->isValidSort($request_sort)) { - $sort_key = $request_sort; - } - - $this->sortKey = $sort_key; - } - - private function getDefaultSort(PhabricatorProject $project) { - $default_sort = $project->getDefaultWorkboardSort(); - - if ($this->isValidSort($default_sort)) { - return $default_sort; - } - - return PhabricatorProjectColumnNaturalOrder::ORDERKEY; - } - - private function getDefaultFilter(PhabricatorProject $project) { - $default_filter = $project->getDefaultWorkboardFilter(); - - if (strlen($default_filter)) { - return $default_filter; - } - - return 'open'; - } - - private function isValidSort($sort) { - $map = PhabricatorProjectColumnOrder::getEnabledOrders(); - return isset($map[$sort]); - } - private function buildSortMenu( PhabricatorUser $viewer, PhabricatorProject $project, $sort_key, array $ordering_map) { - $base_uri = $this->getURIWithState(); + $state = $this->getViewState(); + $base_uri = $state->newWorkboardURI(); $items = array(); foreach ($ordering_map as $key => $ordering) { @@ -997,6 +942,7 @@ final class PhabricatorProjectBoardViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $state = $this->getViewState(); $id = $project->getID(); @@ -1026,12 +972,12 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); if ($show_hidden) { - $hidden_uri = $this->getURIWithState() + $hidden_uri = $state->newWorkboardURI() ->removeQueryParam('hidden'); $hidden_icon = 'fa-eye-slash'; $hidden_text = pht('Hide Hidden Columns'); } else { - $hidden_uri = $this->getURIWithState() + $hidden_uri = $state->newWorkboardURI() ->replaceQueryParam('hidden', 'true'); $hidden_icon = 'fa-eye'; $hidden_text = pht('Show Hidden Columns'); @@ -1307,41 +1253,11 @@ final class PhabricatorProjectBoardViewController * @return PhutilURI URI with state parameters. */ private function getURIWithState($base = null, $force = false) { - $project = $this->getProject(); - if ($base === null) { - $base = $this->getRequest()->getPath(); + $base = $this->getProject()->getWorkboardURI(); } - $base = new PhutilURI($base); - - if ($force || ($this->sortKey != $this->getDefaultSort($project))) { - if ($this->sortKey !== null) { - $base->replaceQueryParam('order', $this->sortKey); - } else { - $base->removeQueryParam('order'); - } - } else { - $base->removeQueryParam('order'); - } - - if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { - if ($this->queryKey !== null) { - $base->replaceQueryParam('filter', $this->queryKey); - } else { - $base->removeQueryParam('filter'); - } - } else { - $base->removeQueryParam('filter'); - } - - if ($this->showHidden) { - $base->replaceQueryParam('hidden', 'true'); - } else { - $base->removeQueryParam('hidden'); - } - - return $base; + return $this->getViewState()->newURI($base, $force); } private function buildInitializeContent(PhabricatorProject $project) { diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php new file mode 100644 index 0000000000..6a037d1400 --- /dev/null +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -0,0 +1,150 @@ +project = $project; + return $this; + } + + public function getProject() { + return $this->project; + } + + public function readFromRequest(AphrontRequest $request) { + if ($request->getExists('hidden')) { + $this->requestState['hidden'] = $request->getBool('hidden'); + } + + if ($request->getExists('order')) { + $this->requestState['order'] = $request->getStr('order'); + } + + // On some pathways, the search engine query key may be specified with + // either a "?filter=X" query parameter or with a "/query/X/" URI + // component. If both are present, the URI component is controlling. + + // In particular, the "queryKey" URI parameter is used by + // "buildSavedQueryFromRequest()" when we are building custom board filters + // by invoking SearchEngine code. + + if ($request->getExists('filter')) { + $this->requestState['filter'] = $request->getStr('filter'); + } + + if (strlen($request->getURIData('queryKey'))) { + $this->requestState['filter'] = $request->getURIData('queryKey'); + } + + return $this; + } + + public function newWorkboardURI($path = null) { + $project = $this->getProject(); + $uri = urisprintf('%p%p', $project->getWorkboardURI(), $path); + return $this->newURI($uri); + } + + public function newURI($path, $force = false) { + $project = $this->getProject(); + $uri = new PhutilURI($path); + + $request_order = $this->getOrder(); + $default_order = $this->getDefaultOrder(); + if ($force || ($request_order !== $default_order)) { + $request_value = idx($this->requestState, 'order'); + if ($request_value !== null) { + $uri->replaceQueryParam('order', $request_value); + } else { + $uri->removeQueryParam('order'); + } + } else { + $uri->removeQueryParam('order'); + } + + $request_query = $this->getQueryKey(); + $default_query = $this->getDefaultQueryKey(); + if ($force || ($request_query !== $default_query)) { + $request_value = idx($this->requestState, 'filter'); + if ($request_value !== null) { + $uri->replaceQueryParam('filter', $request_value); + } else { + $uri->removeQueryParam('filter'); + } + } else { + $uri->removeQueryParam('filter'); + } + + if ($this->getShowHidden()) { + $uri->replaceQueryParam('hidden', 'true'); + } else { + $uri->removeQueryParam('hidden'); + } + + return $uri; + } + + public function getShowHidden() { + $request_show = idx($this->requestState, 'hidden'); + + if ($request_show !== null) { + return $request_show; + } + + return false; + } + + public function getOrder() { + $request_order = idx($this->requestState, 'order'); + if ($request_order !== null) { + if ($this->isValidOrder($request_order)) { + return $request_order; + } + } + + return $this->getDefaultOrder(); + } + + public function getQueryKey() { + $request_query = idx($this->requestState, 'filter'); + if (strlen($request_query)) { + return $request_query; + } + + return $this->getDefaultQueryKey(); + } + + private function isValidOrder($order) { + $map = PhabricatorProjectColumnOrder::getEnabledOrders(); + return isset($map[$order]); + } + + private function getDefaultOrder() { + $project = $this->getProject(); + + $default_order = $project->getDefaultWorkboardSort(); + + if ($this->isValidOrder($default_order)) { + return $default_order; + } + + return PhabricatorProjectColumnNaturalOrder::ORDERKEY; + } + + private function getDefaultQueryKey() { + $project = $this->getProject(); + + $default_query = $project->getDefaultWorkboardFilter(); + + if (strlen($default_query)) { + return $default_query; + } + + return 'open'; + } + +} From 9e096cd274b0e22e3c8048ef847ef61be4a6f147 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 15:23:58 -0700 Subject: [PATCH 06/54] Give the workboard "default" workflows more modern state handling Summary: Depends on D20628. Ref T4900. Currently, the "Save Current Order/Filter As Default" flows on workboards duplicate some state construction, and require parameters to be passed to them explicitly. Now that state management is separate, they can reuse a bit more code and be made to look more like other similar controllers. Test Plan: - Changed the default order of a workboard. - Changed the default filter of a workboard. - Changed the order of a board to something non-default, then changed the filter, then saved the new filter as the default. Saw the modified order preserved and the modified filter removed, so I ended up in the right ("most correct") place: on the board, with my custom order in a URI parameter, and no filter URI parameter so I could see my new default filter behavior. This is an edge case that's not terribly important to get right, but we do get it right. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20629 --- src/__phutil_library_map__.php | 4 +- .../PhabricatorProjectApplication.php | 4 +- .../PhabricatorProjectBoardController.php | 13 ++++- ...bricatorProjectBoardDefaultController.php} | 55 ++++++++----------- .../PhabricatorProjectBoardViewController.php | 10 ++-- .../PhabricatorProjectController.php | 16 ++++++ .../state/PhabricatorWorkboardViewState.php | 4 ++ 7 files changed, 63 insertions(+), 43 deletions(-) rename src/applications/project/controller/{PhabricatorProjectDefaultController.php => PhabricatorProjectBoardDefaultController.php} (60%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7f0ef2afde..ff0f462807 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4158,6 +4158,7 @@ phutil_register_library_map(array( 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 'PhabricatorProjectBoardBackgroundController' => 'applications/project/controller/PhabricatorProjectBoardBackgroundController.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', + 'PhabricatorProjectBoardDefaultController' => 'applications/project/controller/PhabricatorProjectBoardDefaultController.php', 'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php', @@ -4207,7 +4208,6 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php', - 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php', 'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php', @@ -10422,6 +10422,7 @@ phutil_register_library_map(array( 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardBackgroundController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', + 'PhabricatorProjectBoardDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController', @@ -10484,7 +10485,6 @@ phutil_register_library_map(array( 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource', - 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectDropEffect' => 'Phobject', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index d8e78c5f7d..75df0d9a10 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -90,6 +90,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardManageController', 'background/' => 'PhabricatorProjectBoardBackgroundController', + 'default/(?P[^/]+)/' + => 'PhabricatorProjectBoardDefaultController', ), 'column/' => array( 'remove/(?P\d+)/' => @@ -112,8 +114,6 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectSilenceController', 'warning/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectWarningController', - 'default/(?P[1-9]\d*)/(?P[^/]+)/' - => 'PhabricatorProjectDefaultController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectViewController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index 394f8a2fb8..73c0d9e307 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -13,7 +13,7 @@ abstract class PhabricatorProjectBoardController return $this->viewState; } - final private function newViewState() { + private function newViewState() { $project = $this->getProject(); $request = $this->getRequest(); @@ -22,4 +22,15 @@ abstract class PhabricatorProjectBoardController ->readFromRequest($request); } + final protected function newBoardDialog() { + $dialog = $this->newDialog(); + + $state = $this->getViewState(); + foreach ($state->getQueryParameters() as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php similarity index 60% rename from src/applications/project/controller/PhabricatorProjectDefaultController.php rename to src/applications/project/controller/PhabricatorProjectBoardDefaultController.php index 2c7a47b2df..5248f7f8b3 100644 --- a/src/applications/project/controller/PhabricatorProjectDefaultController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php @@ -1,25 +1,20 @@ getViewer(); - $project_id = $request->getURIData('projectID'); - $project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withIDs(array($project_id)) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); + $response = $this->loadProjectForEdit(); + if ($response) { + return $response; } - $this->setProject($project); + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + $remove_param = null; $target = $request->getURIData('target'); switch ($target) { @@ -31,8 +26,10 @@ final class PhabricatorProjectDefaultController 'the board.'); $button = pht('Save Default Filter'); - $xaction_value = $request->getStr('filter'); + $xaction_value = $state->getQueryKey(); $xaction_type = PhabricatorProjectFilterTransaction::TRANSACTIONTYPE; + + $remove_param = 'filter'; break; case 'sort': $title = pht('Set Board Default Order'); @@ -42,8 +39,10 @@ final class PhabricatorProjectDefaultController 'the board.'); $button = pht('Save Default Order'); - $xaction_value = $request->getStr('order'); + $xaction_value = $state->getOrder(); $xaction_type = PhabricatorProjectSortTransaction::TRANSACTIONTYPE; + + $remove_param = 'order'; break; default: return new Aphront404Response(); @@ -51,12 +50,6 @@ final class PhabricatorProjectDefaultController $id = $project->getID(); - $view_uri = $this->getApplicationURI("board/{$id}/"); - $view_uri = new PhutilURI($view_uri); - foreach ($request->getPassthroughRequestData() as $key => $value) { - $view_uri->replaceQueryParam($key, $value); - } - if ($request->isFormPost()) { $xactions = array(); @@ -71,20 +64,18 @@ final class PhabricatorProjectDefaultController ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); - return id(new AphrontRedirectResponse())->setURI($view_uri); + // If the parameter we just modified is present in the query string, + // throw it away so the user is redirected back to the default view of + // the board, allowing them to see the new default behavior. + $board_uri->removeQueryParam($remove_param); + + return id(new AphrontRedirectResponse())->setURI($board_uri); } - $dialog = $this->newDialog() + return $this->newBoardDialog() ->setTitle($title) ->appendChild($body) - ->setDisableWorkflowOnCancel(true) - ->addCancelButton($view_uri) + ->addCancelButton($board_uri) ->addSubmitButton($title); - - foreach ($request->getPassthroughRequestData() as $key => $value) { - $dialog->addHiddenInput($key, $value); - } - - return $dialog; } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index f06fd9c52d..d1d41e926a 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -797,9 +797,7 @@ final class PhabricatorProjectBoardViewController $id = $project->getID(); - $save_uri = "default/{$id}/sort/"; - $save_uri = $this->getApplicationURI($save_uri); - $save_uri = $this->getURIWithState($save_uri, $force = true); + $save_uri = $state->newWorkboardURI('default/sort/'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -842,6 +840,8 @@ final class PhabricatorProjectBoardViewController PhabricatorApplicationSearchEngine $engine, $query_key) { + $state = $this->getViewState(); + $named = array( 'open' => pht('Open Tasks'), 'all' => pht('All Tasks'), @@ -898,9 +898,7 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true) ->setName(pht('Advanced Filter...')); - $save_uri = "default/{$id}/filter/"; - $save_uri = $this->getApplicationURI($save_uri); - $save_uri = $this->getURIWithState($save_uri, $force = true); + $save_uri = $state->newWorkboardURI('default/filter/'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 8551a09cbf..4391a61ff3 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -16,6 +16,21 @@ abstract class PhabricatorProjectController extends PhabricatorController { } protected function loadProject() { + return $this->loadProjectWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } + + protected function loadProjectForEdit() { + return $this->loadProjectWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + } + + private function loadProjectWithCapabilities(array $capabilities) { $viewer = $this->getViewer(); $request = $this->getRequest(); @@ -35,6 +50,7 @@ abstract class PhabricatorProjectController extends PhabricatorController { $query = id(new PhabricatorProjectQuery()) ->setViewer($viewer) + ->requireCapabilities($capabilities) ->needMembers(true) ->needWatchers(true) ->needImages(true) diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php index 6a037d1400..d3f1e87676 100644 --- a/src/applications/project/state/PhabricatorWorkboardViewState.php +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -147,4 +147,8 @@ final class PhabricatorWorkboardViewState return 'open'; } + public function getQueryParameters() { + return $this->requestState; + } + } From 577020aea951b6a49bd445c93b8ca3e66da0eee7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 30 Jun 2019 08:04:03 -0700 Subject: [PATCH 07/54] Move workboard "filter" workflow to a separate controller Summary: Depends on D20629. Ref T4900. Currently, the "Advanced Filter..." workflow on workboards (where you build a custom query) is inline in the main board controller. This is because the filter flow depends on some of the board view state: we want to start with the current filter applied to the board, and preserve other state after you change the filter. Now that `ViewState` can handle state management, we can separate this stuff out pretty easily. Test Plan: - Changed filters on a board. - Applied a custom filter to a board. - Changed the ordering of a board, then applied a custom filter. Verified "Cancel" and "Apply Filter" both preserve the order state. - Changed the ordering of a board, then applied a custom filter, intentionally making a mistake in configuring the filter by entering an invalid date. Saw a dialog with an error. After correcting the error, saw state preserved properly. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20632 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 3 +- .../PhabricatorProjectBoardController.php | 2 +- ...abricatorProjectBoardDefaultController.php | 2 +- ...habricatorProjectBoardFilterController.php | 56 ++++++++++++++++ .../PhabricatorProjectBoardViewController.php | 67 +++---------------- .../state/PhabricatorWorkboardViewState.php | 61 +++++++++++++++++ 7 files changed, 131 insertions(+), 62 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectBoardFilterController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ff0f462807..a55de862d8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4160,6 +4160,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', 'PhabricatorProjectBoardDefaultController' => 'applications/project/controller/PhabricatorProjectBoardDefaultController.php', 'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php', + 'PhabricatorProjectBoardFilterController' => 'applications/project/controller/PhabricatorProjectBoardFilterController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', @@ -10424,6 +10425,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardFilterController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 75df0d9a10..39ab4b3eb1 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -66,7 +66,6 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'subprojects/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectsController', 'board/(?P[1-9]\d*)/'. - '(?Pfilter/)?'. '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', @@ -92,6 +91,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardBackgroundController', 'default/(?P[^/]+)/' => 'PhabricatorProjectBoardDefaultController', + 'filter/(?:query/(?P[^/]+)/)?' + => 'PhabricatorProjectBoardFilterController', ), 'column/' => array( 'remove/(?P\d+)/' => diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index 73c0d9e307..5427d1b93f 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -22,7 +22,7 @@ abstract class PhabricatorProjectBoardController ->readFromRequest($request); } - final protected function newBoardDialog() { + final protected function newWorkboardDialog() { $dialog = $this->newDialog(); $state = $this->getViewState(); diff --git a/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php b/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php index 5248f7f8b3..c531105a0d 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardDefaultController.php @@ -72,7 +72,7 @@ final class PhabricatorProjectBoardDefaultController return id(new AphrontRedirectResponse())->setURI($board_uri); } - return $this->newBoardDialog() + return $this->newWorkboardDialog() ->setTitle($title) ->appendChild($body) ->addCancelButton($board_uri) diff --git a/src/applications/project/controller/PhabricatorProjectBoardFilterController.php b/src/applications/project/controller/PhabricatorProjectBoardFilterController.php new file mode 100644 index 0000000000..ee69cf3ae1 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardFilterController.php @@ -0,0 +1,56 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $search_engine = $state->getSearchEngine(); + + $is_submit = $request->isFormPost(); + + if ($is_submit) { + $saved_query = $search_engine->buildSavedQueryFromRequest($request); + $search_engine->saveQuery($saved_query); + } else { + $saved_query = $state->getSavedQuery(); + if (!$saved_query) { + return new Aphront404Response(); + } + } + + $filter_form = id(new AphrontFormView()) + ->setUser($viewer); + + $search_engine->buildSearchForm($filter_form, $saved_query); + + $errors = $search_engine->getErrors(); + + if ($is_submit && !$errors) { + $query_key = $saved_query->getQueryKey(); + + $state->setQueryKey($query_key); + $board_uri = $state->newWorkboardURI(); + + return id(new AphrontRedirectResponse())->setURI($board_uri); + } + + return $this->newWorkboardDialog() + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setTitle(pht('Advanced Filter')) + ->appendChild($filter_form->buildLayoutView()) + ->setErrors($errors) + ->addSubmitButton(pht('Apply Filter')) + ->addCancelButton($board_uri); + } +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index d1d41e926a..3d7ee620d8 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -21,68 +21,17 @@ final class PhabricatorProjectBoardViewController $state = $this->getViewState(); $board_uri = $project->getWorkboardURI(); - $search_engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer) - ->setBaseURI($board_uri) - ->setIsBoardView(true); - - if ($request->isFormPost() - && !$request->getBool('initialize') - && !$request->getStr('move') - && !$request->getStr('queryColumnID')) { - $saved = $search_engine->buildSavedQueryFromRequest($request); - $search_engine->saveQuery($saved); - $filter_form = id(new AphrontFormView()) - ->setUser($viewer); - $search_engine->buildSearchForm($filter_form, $saved); - if ($search_engine->getErrors()) { - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FULL) - ->setTitle(pht('Advanced Filter')) - ->appendChild($filter_form->buildLayoutView()) - ->setErrors($search_engine->getErrors()) - ->setSubmitURI($board_uri) - ->addSubmitButton(pht('Apply Filter')) - ->addCancelButton($board_uri); - } - - $query_key = $saved->getQueryKey(); - $results_uri = $search_engine->getQueryResultsPageURI($query_key); - $results_uri = $state->newURI($results_uri); - - return id(new AphrontRedirectResponse())->setURI($results_uri); - } - + $search_engine = $state->getSearchEngine(); $query_key = $state->getQueryKey(); - - $custom_query = null; - if ($search_engine->isBuiltinQuery($query_key)) { - $saved = $search_engine->buildSavedQueryFromBuiltin($query_key); - } else { - $saved = id(new PhabricatorSavedQueryQuery()) - ->setViewer($viewer) - ->withQueryKeys(array($query_key)) - ->executeOne(); - - if (!$saved) { - return new Aphront404Response(); - } - - $custom_query = $saved; + $saved = $state->getSavedQuery(); + if (!$saved) { + return new Aphront404Response(); } - if ($request->getURIData('filter')) { - $filter_form = id(new AphrontFormView()) - ->setUser($viewer); - $search_engine->buildSearchForm($filter_form, $saved); - - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FULL) - ->setTitle(pht('Advanced Filter')) - ->appendChild($filter_form->buildLayoutView()) - ->setSubmitURI($board_uri) - ->addSubmitButton(pht('Apply Filter')) - ->addCancelButton($board_uri); + if ($saved->getID()) { + $custom_query = $saved; + } else { + $custom_query = null; } $task_query = $search_engine->buildQueryFromSavedQuery($saved); diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php index d3f1e87676..60f1ea13bc 100644 --- a/src/applications/project/state/PhabricatorWorkboardViewState.php +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -3,8 +3,11 @@ final class PhabricatorWorkboardViewState extends Phobject { + private $viewer; private $project; private $requestState = array(); + private $savedQuery; + private $searchEngine; public function setProject(PhabricatorProject $project) { $this->project = $project; @@ -40,9 +43,62 @@ final class PhabricatorWorkboardViewState $this->requestState['filter'] = $request->getURIData('queryKey'); } + $this->viewer = $request->getViewer(); + return $this; } + public function getViewer() { + return $this->viewer; + } + + public function getSavedQuery() { + if ($this->savedQuery === null) { + $this->savedQuery = $this->newSavedQuery(); + } + + return $this->savedQuery; + } + + private function newSavedQuery() { + $search_engine = $this->getSearchEngine(); + $query_key = $this->getQueryKey(); + $viewer = $this->getViewer(); + + if ($search_engine->isBuiltinQuery($query_key)) { + $saved_query = $search_engine->buildSavedQueryFromBuiltin($query_key); + } else { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } + + return $saved_query; + } + + public function getSearchEngine() { + if ($this->searchEngine === null) { + $this->searchEngine = $this->newSearchEngine(); + } + + return $this->searchEngine; + } + + private function newSearchEngine() { + $viewer = $this->getViewer(); + + // TODO: This URI is not fully state-preserving, because "SearchEngine" + // does not preserve URI parameters when constructing some URIs at time of + // writing. + $board_uri = $this->getProject()->getWorkboardURI(); + + return id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer) + ->setBaseURI($board_uri) + ->setIsBoardView(true); + } + public function newWorkboardURI($path = null) { $project = $this->getProject(); $uri = urisprintf('%p%p', $project->getWorkboardURI(), $path); @@ -118,6 +174,11 @@ final class PhabricatorWorkboardViewState return $this->getDefaultQueryKey(); } + public function setQueryKey($query_key) { + $this->requestState['filter'] = $query_key; + return $this; + } + private function isValidOrder($order) { $map = PhabricatorProjectColumnOrder::getEnabledOrders(); return isset($map[$order]); From 9ea7227f0fc9298d13b8ea6a6414ff8f344b382d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 05:36:43 -0700 Subject: [PATCH 08/54] Move workboard "View as Query" workflow to a separate controller Summary: Depends on D20632. Ref T4900. As with other workflows on the board controller, this one is currently in the giant main "do everything" method. Move it to a separate controller. This makes one material improvement: previously, we built the full board and did layout on all the cards before building the query. However, we do not actually need to do this: we don't need the cards. Instead, just do layout without handing over any card PHIDs. This is slightly faster, particularly on large boards. Test Plan: - Clicked "View as Query" on a board, got a query page for the column. - Applied a custom filter, then clicked "View as Query" on a board. Got a query page merging the two filters. - Applied a custom filter, then clicked "Veiw as Query" on a board, in a subproject column. Got a query page merging the two filters, respecting the project-ness of the column. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20633 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 2 + .../PhabricatorProjectBoardViewController.php | 45 +----------- ...icatorProjectColumnViewQueryController.php | 72 +++++++++++++++++++ 4 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a55de862d8..ddba35ae95 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4198,6 +4198,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 'PhabricatorProjectColumnTransactionType' => 'applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php', 'PhabricatorProjectColumnTriggerTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php', + 'PhabricatorProjectColumnViewQueryController' => 'applications/project/controller/PhabricatorProjectColumnViewQueryController.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', @@ -10473,6 +10474,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectColumnTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorProjectColumnTriggerTransaction' => 'PhabricatorProjectColumnTransactionType', + 'PhabricatorProjectColumnViewQueryController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( 'PhabricatorProjectStandardCustomField', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 39ab4b3eb1..ce52255586 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -79,6 +79,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectColumnHideController', 'column/(?:(?P\d+)/)?' => 'PhabricatorProjectColumnDetailController', + 'viewquery/(?P\d+)/' + => 'PhabricatorProjectColumnViewQueryController', 'import/' => 'PhabricatorProjectBoardImportController', 'reorder/' diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 3d7ee620d8..06ba8efc14 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -122,46 +122,6 @@ final class PhabricatorProjectBoardViewController ->appendChild($content); } - // If the user wants to turn a particular column into a query, build an - // apropriate filter and redirect them to the query results page. - $query_column_id = $request->getInt('queryColumnID'); - if ($query_column_id) { - $column_id_map = mpull($columns, null, 'getID'); - $query_column = idx($column_id_map, $query_column_id); - if (!$query_column) { - return new Aphront404Response(); - } - - // Create a saved query to combine the active filter on the workboard - // with the column filter. If the user currently has constraints on the - // board, we want to add a new column or project constraint, not - // completely replace the constraints. - $saved_query = $saved->newCopy(); - - if ($query_column->getProxyPHID()) { - $project_phids = $saved_query->getParameter('projectPHIDs'); - if (!$project_phids) { - $project_phids = array(); - } - $project_phids[] = $query_column->getProxyPHID(); - $saved_query->setParameter('projectPHIDs', $project_phids); - } else { - $saved_query->setParameter( - 'columnPHIDs', - array($query_column->getPHID())); - } - - $search_engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer); - $search_engine->saveQuery($saved_query); - - $query_key = $saved_query->getQueryKey(); - $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); - - return id(new AphrontRedirectResponse()) - ->setURI($query_uri); - } - $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) @@ -1004,6 +964,7 @@ final class PhabricatorProjectBoardViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $state = $this->getViewState(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -1062,8 +1023,8 @@ final class PhabricatorProjectBoardViewController ->setHref($batch_move_uri) ->setWorkflow(true); - $query_uri = $request->getRequestURI(); - $query_uri->replaceQueryParam('queryColumnID', $column->getID()); + $query_uri = urisprintf('viewquery/%d/', $column->getID()); + $query_uri = $state->newWorkboardURI($query_uri); $column_items[] = id(new PhabricatorActionView()) ->setName(pht('View as Query')) diff --git a/src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php b/src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php new file mode 100644 index 0000000000..7b8a304daa --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnViewQueryController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + // NOTE: We're performing layout without handing the "LayoutEngine" any + // object PHIDs. We only want to get access to the column object the user + // is trying to query, so we do not need to actually position any cards on + // the board. + + $board_phid = $project->getPHID(); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $column = idx($columns, $column_id); + if (!$column) { + return new Aphront404Response(); + } + + // Create a saved query to combine the active filter on the workboard + // with the column filter. If the user currently has constraints on the + // board, we want to add a new column or project constraint, not + // completely replace the constraints. + $default_query = $state->getSavedQuery(); + $saved_query = $default_query->newCopy(); + + if ($column->getProxyPHID()) { + $project_phids = $saved_query->getParameter('projectPHIDs'); + if (!$project_phids) { + $project_phids = array(); + } + $project_phids[] = $column->getProxyPHID(); + $saved_query->setParameter('projectPHIDs', $project_phids); + } else { + $saved_query->setParameter( + 'columnPHIDs', + array($column->getPHID())); + } + + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); + + return id(new AphrontRedirectResponse()) + ->setURI($query_uri); + } + +} From ec352b1b31bb31c4ee82f69dd008f3c4961a4107 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 06:11:44 -0700 Subject: [PATCH 09/54] Move workboard "Bulk Edit Tasks" workflow to a separate controller Summary: Depends on D20633. Ref T4900. Separate the "Bulk Edit Tasks..." flow out of the main workboard controller. Test Plan: - Used "Bulk Edit Tasks" on a column with some tasks, got an appropraite edit operation. - Used "Bulk Edit Tasks" on an empty column, got an error. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20634 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 2 + .../PhabricatorProjectBoardViewController.php | 119 +++--------------- ...ricatorProjectColumnBulkEditController.php | 72 +++++++++++ .../state/PhabricatorWorkboardViewState.php | 76 +++++++++++ 5 files changed, 166 insertions(+), 105 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ddba35ae95..18c466d900 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4172,6 +4172,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnAuthorOrder' => 'applications/project/order/PhabricatorProjectColumnAuthorOrder.php', + 'PhabricatorProjectColumnBulkEditController' => 'applications/project/controller/PhabricatorProjectColumnBulkEditController.php', 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', @@ -10445,6 +10446,7 @@ phutil_register_library_map(array( 'PhabricatorConduitResultInterface', ), 'PhabricatorProjectColumnAuthorOrder' => 'PhabricatorProjectColumnOrder', + 'PhabricatorProjectColumnBulkEditController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index ce52255586..321b407bb1 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -81,6 +81,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectColumnDetailController', 'viewquery/(?P\d+)/' => 'PhabricatorProjectColumnViewQueryController', + 'bulk/(?P\d+)/' + => 'PhabricatorProjectColumnBulkEditController', 'import/' => 'PhabricatorProjectBoardImportController', 'reorder/' diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 06ba8efc14..f959f0d198 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -3,8 +3,6 @@ final class PhabricatorProjectBoardViewController extends PhabricatorProjectBoardController { - const BATCH_EDIT_ALL = 'all'; - public function shouldAllowPublic() { return true; } @@ -34,42 +32,9 @@ final class PhabricatorProjectBoardViewController $custom_query = null; } - $task_query = $search_engine->buildQueryFromSavedQuery($saved); - - $select_phids = array($project->getPHID()); - if ($project->getHasSubprojects() || $project->getHasMilestones()) { - $descendants = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withAncestorProjectPHIDs($select_phids) - ->execute(); - foreach ($descendants as $descendant) { - $select_phids[] = $descendant->getPHID(); - } - } - - $tasks = $task_query - ->withEdgeLogicPHIDs( - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, - PhabricatorQueryConstraint::OPERATOR_ANCESTOR, - array($select_phids)) - ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) - ->setViewer($viewer) - ->execute(); - $tasks = mpull($tasks, null, 'getPHID'); + $layout_engine = $state->getLayoutEngine(); $board_phid = $project->getPHID(); - - // Regardless of display order, pass tasks to the layout engine in ID order - // so layout is consistent. - $board_tasks = msort($tasks, 'getID'); - - $layout_engine = id(new PhabricatorBoardLayoutEngine()) - ->setViewer($viewer) - ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs(array_keys($board_tasks)) - ->setFetchAllBoards(true) - ->executeLayout(); - $columns = $layout_engine->getColumns($board_phid); if (!$columns || !$project->getHasWorkboard()) { $has_normal_columns = false; @@ -122,67 +87,13 @@ final class PhabricatorProjectBoardViewController ->appendChild($content); } + $tasks = $state->getObjects(); + $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($tasks); - // If this is a batch edit, select the editable tasks in the chosen column - // and ship the user into the batch editor. - $batch_edit = $request->getStr('batch'); - if ($batch_edit) { - if ($batch_edit !== self::BATCH_EDIT_ALL) { - $column_id_map = mpull($columns, null, 'getID'); - $batch_column = idx($column_id_map, $batch_edit); - if (!$batch_column) { - return new Aphront404Response(); - } - - $batch_task_phids = $layout_engine->getColumnObjectPHIDs( - $board_phid, - $batch_column->getPHID()); - - foreach ($batch_task_phids as $key => $batch_task_phid) { - if (empty($task_can_edit_map[$batch_task_phid])) { - unset($batch_task_phids[$key]); - } - } - - $batch_tasks = array_select_keys($tasks, $batch_task_phids); - } else { - $batch_tasks = $task_can_edit_map; - } - - if (!$batch_tasks) { - $cancel_uri = $state->newWorkboardURI(); - return $this->newDialog() - ->setTitle(pht('No Editable Tasks')) - ->appendParagraph( - pht( - 'The selected column contains no visible tasks which you '. - 'have permission to edit.')) - ->addCancelButton($board_uri); - } - - // Create a saved query to hold the working set. This allows us to get - // around URI length limitations with a long "?ids=..." query string. - // For details, see T10268. - $search_engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer); - - $saved_query = $search_engine->newSavedQuery(); - $saved_query->setParameter('ids', mpull($batch_tasks, 'getID')); - $search_engine->saveQuery($saved_query); - - $query_key = $saved_query->getQueryKey(); - - $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); - $bulk_uri->replaceQueryParam('board', $project->getID()); - - return id(new AphrontRedirectResponse()) - ->setURI($bulk_uri); - } - $move_id = $request->getStr('move'); if (strlen($move_id)) { $column_id_map = mpull($columns, null, 'getID'); @@ -426,11 +337,13 @@ final class PhabricatorProjectBoardViewController } } + $container_phids = $state->getBoardContainerPHIDs(); + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) ->setObjects(array_select_keys($tasks, $visible_phids)) ->setEditMap($task_can_edit_map) - ->setExcludedProjectPHIDs($select_phids); + ->setExcludedProjectPHIDs($container_phids); $templates = array(); $all_tasks = array(); @@ -912,13 +825,6 @@ final class PhabricatorProjectBoardViewController ->setName(pht('Manage Workboard')) ->setHref($manage_uri); - $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->replaceQueryParam('batch', self::BATCH_EDIT_ALL); - $can_batch_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), - ManiphestBulkEditCapability::CAPABILITY); - $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { @@ -1002,9 +908,12 @@ final class PhabricatorProjectBoardViewController $column_items[] = id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER); - $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->replaceQueryParam('batch', $column->getID()); - $can_batch_edit = PhabricatorPolicyFilter::hasCapability( + $bulk_edit_uri = $state->newWorkboardURI( + urisprintf( + 'bulk/%d/', + $column->getID())); + + $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); @@ -1012,8 +921,8 @@ final class PhabricatorProjectBoardViewController $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-list-ul') ->setName(pht('Bulk Edit Tasks...')) - ->setHref($batch_edit_uri) - ->setDisabled(!$can_batch_edit); + ->setHref($bulk_edit_uri) + ->setDisabled(!$can_bulk_edit); $batch_move_uri = $request->getRequestURI(); $batch_move_uri->replaceQueryParam('move', $column->getID()); diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php new file mode 100644 index 0000000000..0d9c2ff78f --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkEditController.php @@ -0,0 +1,72 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $bulk_column = idx($columns, $column_id); + if (!$bulk_column) { + return new Aphront404Response(); + } + + $bulk_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $bulk_column->getPHID()); + + $tasks = $state->getObjects(); + + $bulk_tasks = array_select_keys($tasks, $bulk_task_phids); + + $bulk_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($bulk_tasks); + + if (!$bulk_tasks) { + return $this->newDialog() + ->setTitle(pht('No Editable Tasks')) + ->appendParagraph( + pht( + 'The selected column contains no visible tasks which you '. + 'have permission to edit.')) + ->addCancelButton($board_uri); + } + + // Create a saved query to hold the working set. This allows us to get + // around URI length limitations with a long "?ids=..." query string. + // For details, see T10268. + $search_engine = id(new ManiphestTaskSearchEngine()) + ->setViewer($viewer); + + $saved_query = $search_engine->newSavedQuery(); + $saved_query->setParameter('ids', mpull($bulk_tasks, 'getID')); + $search_engine->saveQuery($saved_query); + + $query_key = $saved_query->getQueryKey(); + + $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); + $bulk_uri->replaceQueryParam('board', $project->getID()); + + return id(new AphrontRedirectResponse()) + ->setURI($bulk_uri); + } + +} diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php index 60f1ea13bc..555ed575a6 100644 --- a/src/applications/project/state/PhabricatorWorkboardViewState.php +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -8,6 +8,8 @@ final class PhabricatorWorkboardViewState private $requestState = array(); private $savedQuery; private $searchEngine; + private $layoutEngine; + private $objects; public function setProject(PhabricatorProject $project) { $this->project = $project; @@ -212,4 +214,78 @@ final class PhabricatorWorkboardViewState return $this->requestState; } + public function getLayoutEngine() { + if ($this->layoutEngine === null) { + $this->layoutEngine = $this->newLayoutEngine(); + } + return $this->layoutEngine; + } + + private function newLayoutEngine() { + $project = $this->getProject(); + $viewer = $this->getViewer(); + + $board_phid = $project->getPHID(); + $objects = $this->getObjects(); + + // Regardless of display order, pass tasks to the layout engine in ID order + // so layout is consistent. + $objects = msort($objects, 'getID'); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setObjectPHIDs(array_keys($objects)) + ->setBoardPHIDs(array($board_phid)) + ->setFetchAllBoards(true) + ->executeLayout(); + + return $layout_engine; + } + + public function getBoardContainerPHIDs() { + $project = $this->getProject(); + $viewer = $this->getViewer(); + + $container_phids = array($project->getPHID()); + if ($project->getHasSubprojects() || $project->getHasMilestones()) { + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($container_phids) + ->execute(); + foreach ($descendants as $descendant) { + $container_phids[] = $descendant->getPHID(); + } + } + + return $container_phids; + } + + public function getObjects() { + if ($this->objects === null) { + $this->objects = $this->newObjects(); + } + + return $this->objects; + } + + private function newObjects() { + $viewer = $this->getViewer(); + $saved_query = $this->getSavedQuery(); + $search_engine = $this->getSearchEngine(); + + $container_phids = $this->getBoardContainerPHIDs(); + + $task_query = $search_engine->buildQueryFromSavedQuery($saved_query) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($container_phids)); + + $tasks = $task_query->execute(); + $tasks = mpull($tasks, null, 'getPHID'); + + return $tasks; + } + } From 24b466cd62d1bd1398e0200838e567bf5801718f Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 07:39:37 -0700 Subject: [PATCH 10/54] Move workboard "Move Tasks to Column..." workflow to a separate controller Summary: Depends on D20634. Ref T4900. Ref T13316. I'm planning to do a bit of additional cleanup here in followups, but this separates the main workflow out of the common controller. Test Plan: - Used "Move Tasks to Column..." to move some tasks on a board. - Tried to move an empty column, hit an error. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13316, T4900 Differential Revision: https://secure.phabricator.com/D20635 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 2 + .../PhabricatorProjectBoardViewController.php | 209 +---------------- ...ricatorProjectColumnBulkMoveController.php | 219 ++++++++++++++++++ 4 files changed, 229 insertions(+), 203 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 18c466d900..963666fe82 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4173,6 +4173,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnAuthorOrder' => 'applications/project/order/PhabricatorProjectColumnAuthorOrder.php', 'PhabricatorProjectColumnBulkEditController' => 'applications/project/controller/PhabricatorProjectColumnBulkEditController.php', + 'PhabricatorProjectColumnBulkMoveController' => 'applications/project/controller/PhabricatorProjectColumnBulkMoveController.php', 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', @@ -10447,6 +10448,7 @@ phutil_register_library_map(array( ), 'PhabricatorProjectColumnAuthorOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnBulkEditController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectColumnBulkMoveController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 321b407bb1..1c04e0c2f9 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -83,6 +83,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectColumnViewQueryController', 'bulk/(?P\d+)/' => 'PhabricatorProjectColumnBulkEditController', + 'bulkmove/(?P\d+)/(?Pproject|column)/' + => 'PhabricatorProjectColumnBulkMoveController', 'import/' => 'PhabricatorProjectBoardImportController', 'reorder/' diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index f959f0d198..e4ddaee4dc 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -94,206 +94,6 @@ final class PhabricatorProjectBoardViewController ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($tasks); - $move_id = $request->getStr('move'); - if (strlen($move_id)) { - $column_id_map = mpull($columns, null, 'getID'); - $move_column = idx($column_id_map, $move_id); - if (!$move_column) { - return new Aphront404Response(); - } - - $move_task_phids = $layout_engine->getColumnObjectPHIDs( - $board_phid, - $move_column->getPHID()); - - foreach ($move_task_phids as $key => $move_task_phid) { - if (empty($task_can_edit_map[$move_task_phid])) { - unset($move_task_phids[$key]); - } - } - - $move_tasks = array_select_keys($tasks, $move_task_phids); - $cancel_uri = $state->newWorkboardURI(); - - if (!$move_tasks) { - return $this->newDialog() - ->setTitle(pht('No Movable Tasks')) - ->appendParagraph( - pht( - 'The selected column contains no visible tasks which you '. - 'have permission to move.')) - ->addCancelButton($cancel_uri); - } - - $move_project_phid = $project->getPHID(); - $move_column_phid = null; - $move_project = null; - $move_column = null; - $columns = null; - $errors = array(); - - if ($request->isFormOrHiSecPost()) { - $move_project_phid = head($request->getArr('moveProjectPHID')); - if (!$move_project_phid) { - $move_project_phid = $request->getStr('moveProjectPHID'); - } - - if (!$move_project_phid) { - if ($request->getBool('hasProject')) { - $errors[] = pht('Choose a project to move tasks to.'); - } - } else { - $target_project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($move_project_phid)) - ->executeOne(); - if (!$target_project) { - $errors[] = pht('You must choose a valid project.'); - } else if (!$project->getHasWorkboard()) { - $errors[] = pht( - 'You must choose a project with a workboard.'); - } else { - $move_project = $target_project; - } - } - - if ($move_project) { - $move_engine = id(new PhabricatorBoardLayoutEngine()) - ->setViewer($viewer) - ->setBoardPHIDs(array($move_project->getPHID())) - ->setFetchAllBoards(true) - ->executeLayout(); - - $columns = $move_engine->getColumns($move_project->getPHID()); - $columns = mpull($columns, null, 'getPHID'); - - foreach ($columns as $key => $column) { - if ($column->isHidden()) { - unset($columns[$key]); - } - } - - $move_column_phid = $request->getStr('moveColumnPHID'); - if (!$move_column_phid) { - if ($request->getBool('hasColumn')) { - $errors[] = pht('Choose a column to move tasks to.'); - } - } else { - if (empty($columns[$move_column_phid])) { - $errors[] = pht( - 'Choose a valid column on the target workboard to move '. - 'tasks to.'); - } else if ($columns[$move_column_phid]->getID() == $move_id) { - $errors[] = pht( - 'You can not move tasks from a column to itself.'); - } else { - $move_column = $columns[$move_column_phid]; - } - } - } - } - - if ($move_column && $move_project) { - foreach ($move_tasks as $move_task) { - $xactions = array(); - - // If we're switching projects, get out of the old project first - // and move to the new project. - if ($move_project->getID() != $project->getID()) { - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue( - 'edge:type', - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) - ->setNewValue( - array( - '-' => array( - $project->getPHID() => $project->getPHID(), - ), - '+' => array( - $move_project->getPHID() => $move_project->getPHID(), - ), - )); - } - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) - ->setNewValue( - array( - array( - 'columnPHID' => $move_column->getPHID(), - ), - )); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($viewer) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->setCancelURI($cancel_uri); - - $editor->applyTransactions($move_task, $xactions); - } - - return id(new AphrontRedirectResponse()) - ->setURI($cancel_uri); - } - - if ($move_project) { - $column_form = id(new AphrontFormView()) - ->setViewer($viewer) - ->appendControl( - id(new AphrontFormSelectControl()) - ->setName('moveColumnPHID') - ->setLabel(pht('Move to Column')) - ->setValue($move_column_phid) - ->setOptions(mpull($columns, 'getDisplayName', 'getPHID'))); - - return $this->newDialog() - ->setTitle(pht('Move Tasks')) - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setErrors($errors) - ->addHiddenInput('move', $move_id) - ->addHiddenInput('moveProjectPHID', $move_project->getPHID()) - ->addHiddenInput('hasColumn', true) - ->addHiddenInput('hasProject', true) - ->appendParagraph( - pht( - 'Choose a column on the %s workboard to move tasks to:', - $viewer->renderHandle($move_project->getPHID()))) - ->appendForm($column_form) - ->addSubmitButton(pht('Move Tasks')) - ->addCancelButton($cancel_uri); - } - - if ($move_project_phid) { - $move_project_phid_value = array($move_project_phid); - } else { - $move_project_phid_value = array(); - } - - $project_form = id(new AphrontFormView()) - ->setViewer($viewer) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setName('moveProjectPHID') - ->setLimit(1) - ->setLabel(pht('Move to Project')) - ->setValue($move_project_phid_value) - ->setDatasource(new PhabricatorProjectDatasource())); - - return $this->newDialog() - ->setTitle(pht('Move Tasks')) - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setErrors($errors) - ->addHiddenInput('move', $move_id) - ->addHiddenInput('hasProject', true) - ->appendForm($project_form) - ->addSubmitButton(pht('Continue')) - ->addCancelButton($cancel_uri); - } - - $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) @@ -924,12 +724,15 @@ final class PhabricatorProjectBoardViewController ->setHref($bulk_edit_uri) ->setDisabled(!$can_bulk_edit); - $batch_move_uri = $request->getRequestURI(); - $batch_move_uri->replaceQueryParam('move', $column->getID()); + $project_move_uri = $state->newWorkboardURI( + urisprintf( + 'bulkmove/%d/project/', + $column->getID())); + $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-arrow-right') ->setName(pht('Move Tasks to Column...')) - ->setHref($batch_move_uri) + ->setHref($project_move_uri) ->setWorkflow(true); $query_uri = urisprintf('viewquery/%d/', $column->getID()); diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php new file mode 100644 index 0000000000..d16aeab157 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php @@ -0,0 +1,219 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + $columns = $layout_engine->getColumns($board_phid); + $columns = mpull($columns, null, 'getID'); + + $column_id = $request->getURIData('columnID'); + $move_column = idx($columns, $column_id); + if (!$move_column) { + return new Aphront404Response(); + } + + $move_task_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $move_column->getPHID()); + + $tasks = $state->getObjects(); + + $move_tasks = array_select_keys($tasks, $move_task_phids); + + $move_tasks = id(new PhabricatorPolicyFilter()) + ->setViewer($viewer) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->apply($move_tasks); + + if (!$move_tasks) { + return $this->newDialog() + ->setTitle(pht('No Movable Tasks')) + ->appendParagraph( + pht( + 'The selected column contains no visible tasks which you '. + 'have permission to move.')) + ->addCancelButton($board_uri); + } + + $move_project_phid = $project->getPHID(); + $move_column_phid = null; + $move_project = null; + $move_column = null; + $columns = null; + $errors = array(); + + if ($request->isFormOrHiSecPost()) { + $move_project_phid = head($request->getArr('moveProjectPHID')); + if (!$move_project_phid) { + $move_project_phid = $request->getStr('moveProjectPHID'); + } + + if (!$move_project_phid) { + if ($request->getBool('hasProject')) { + $errors[] = pht('Choose a project to move tasks to.'); + } + } else { + $target_project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($move_project_phid)) + ->executeOne(); + if (!$target_project) { + $errors[] = pht('You must choose a valid project.'); + } else if (!$project->getHasWorkboard()) { + $errors[] = pht( + 'You must choose a project with a workboard.'); + } else { + $move_project = $target_project; + } + } + + if ($move_project) { + $move_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($move_project->getPHID())) + ->setFetchAllBoards(true) + ->executeLayout(); + + $columns = $move_engine->getColumns($move_project->getPHID()); + $columns = mpull($columns, null, 'getPHID'); + + foreach ($columns as $key => $column) { + if ($column->isHidden()) { + unset($columns[$key]); + } + } + + $move_column_phid = $request->getStr('moveColumnPHID'); + if (!$move_column_phid) { + if ($request->getBool('hasColumn')) { + $errors[] = pht('Choose a column to move tasks to.'); + } + } else { + if (empty($columns[$move_column_phid])) { + $errors[] = pht( + 'Choose a valid column on the target workboard to move '. + 'tasks to.'); + } else if ($columns[$move_column_phid]->getID() == $column_id) { + $errors[] = pht( + 'You can not move tasks from a column to itself.'); + } else { + $move_column = $columns[$move_column_phid]; + } + } + } + } + + if ($move_column && $move_project) { + foreach ($move_tasks as $move_task) { + $xactions = array(); + + // If we're switching projects, get out of the old project first + // and move to the new project. + if ($move_project->getID() != $project->getID()) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) + ->setNewValue( + array( + '-' => array( + $project->getPHID() => $project->getPHID(), + ), + '+' => array( + $move_project->getPHID() => $move_project->getPHID(), + ), + )); + } + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) + ->setNewValue( + array( + array( + 'columnPHID' => $move_column->getPHID(), + ), + )); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setCancelURI($board_uri); + + $editor->applyTransactions($move_task, $xactions); + } + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + if ($move_project) { + $column_form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormSelectControl()) + ->setName('moveColumnPHID') + ->setLabel(pht('Move to Column')) + ->setValue($move_column_phid) + ->setOptions(mpull($columns, 'getDisplayName', 'getPHID'))); + + return $this->newWorkboardDialog() + ->setTitle(pht('Move Tasks')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->addHiddenInput('moveProjectPHID', $move_project->getPHID()) + ->addHiddenInput('hasColumn', true) + ->addHiddenInput('hasProject', true) + ->appendParagraph( + pht( + 'Choose a column on the %s workboard to move tasks to:', + $viewer->renderHandle($move_project->getPHID()))) + ->appendForm($column_form) + ->addSubmitButton(pht('Move Tasks')) + ->addCancelButton($board_uri); + } + + if ($move_project_phid) { + $move_project_phid_value = array($move_project_phid); + } else { + $move_project_phid_value = array(); + } + + $project_form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setName('moveProjectPHID') + ->setLimit(1) + ->setLabel(pht('Move to Project')) + ->setValue($move_project_phid_value) + ->setDatasource(new PhabricatorProjectDatasource())); + + return $this->newWorkboardDialog() + ->setTitle(pht('Move Tasks')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->addHiddenInput('hasProject', true) + ->appendForm($project_form) + ->addSubmitButton(pht('Continue')) + ->addCancelButton($board_uri); + } + +} From 58e2fa0d47fd65028419752151337f9e2b546b2f Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 08:25:30 -0700 Subject: [PATCH 11/54] Differentiate between "Move Tasks to Column..." and "Move Tasks to Project..." in the workboard UI Summary: Depends on D20635. Ref T4900. Fixes T13316. Currently, "Move Tasks to Column..." first prompts you to select a project, then prompts you for a column. The first step is prefilled with the current project, so the common case (moving to another column on the same board) requires you to confirm that you aren't doing an off-project move by clicking "Continue", then you can select a column. This isn't a huge inconvenience and the workflow isn't terribly common, but it's surprising enough that it has come up a few times as a stumbling block. Particularly, we're suggesting to users that they're about to pick a column, then we're asking them to pick a project. The prompt also says "Project: XYZ", not "Project: Keep in current project" or something like that. Smooth this out by splitting the action into two better-cued flows: - "Move Tasks to Project..." is the current flow: pick a project, then pick a column. - The project selection no longer defaults to the current project, since we now expect you to usually use this flow to move tasks to a different project. - "Move Tasks to Column..." prompts you to select a column on the same board. - This just skips step 1 of the workflow. - This now defaults to the current column, which isn't a useful selection, but is more clear. In both cases, the action cue ("Move tasks to X...") now matches what the dialog actually asks you for ("Pick an X"). Test Plan: - Moved tasks across projects and columns within the same project. - Hit all (I think?) the error cases and got sensible error and recovery behavior. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13316, T4900 Differential Revision: https://secure.phabricator.com/D20636 --- .../PhabricatorProjectBoardViewController.php | 49 ++- ...ricatorProjectColumnBulkMoveController.php | 333 ++++++++++-------- 2 files changed, 221 insertions(+), 161 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index e4ddaee4dc..f49eca1beb 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -708,6 +708,36 @@ final class PhabricatorProjectBoardViewController $column_items[] = id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER); + $query_uri = urisprintf('viewquery/%d/', $column->getID()); + $query_uri = $state->newWorkboardURI($query_uri); + + $column_items[] = id(new PhabricatorActionView()) + ->setName(pht('View Tasks as Query')) + ->setIcon('fa-search') + ->setHref($query_uri); + + $column_move_uri = $state->newWorkboardURI( + urisprintf( + 'bulkmove/%d/column/', + $column->getID())); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-arrows-h') + ->setName(pht('Move Tasks to Column...')) + ->setHref($column_move_uri) + ->setWorkflow(true); + + $project_move_uri = $state->newWorkboardURI( + urisprintf( + 'bulkmove/%d/project/', + $column->getID())); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-arrows') + ->setName(pht('Move Tasks to Project...')) + ->setHref($project_move_uri) + ->setWorkflow(true); + $bulk_edit_uri = $state->newWorkboardURI( urisprintf( 'bulk/%d/', @@ -719,29 +749,14 @@ final class PhabricatorProjectBoardViewController ManiphestBulkEditCapability::CAPABILITY); $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-list-ul') + ->setIcon('fa-pencil-square-o') ->setName(pht('Bulk Edit Tasks...')) ->setHref($bulk_edit_uri) ->setDisabled(!$can_bulk_edit); - $project_move_uri = $state->newWorkboardURI( - urisprintf( - 'bulkmove/%d/project/', - $column->getID())); - $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-arrow-right') - ->setName(pht('Move Tasks to Column...')) - ->setHref($project_move_uri) - ->setWorkflow(true); + ->setType(PhabricatorActionView::TYPE_DIVIDER); - $query_uri = urisprintf('viewquery/%d/', $column->getID()); - $query_uri = $state->newWorkboardURI($query_uri); - - $column_items[] = id(new PhabricatorActionView()) - ->setName(pht('View as Query')) - ->setIcon('fa-search') - ->setHref($query_uri); $edit_uri = 'board/'.$project->getID().'/edit/'.$column->getID().'/'; $column_items[] = id(new PhabricatorActionView()) diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php index d16aeab157..2b4c536c8d 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php @@ -11,25 +11,30 @@ final class PhabricatorProjectColumnBulkMoveController return $response; } - $project = $this->getProject(); + // See T13316. If we're operating in "column" mode, we're going to skip + // the prompt for a project and just have the user select a target column. + // In "project" mode, we prompt them for a project first. + $is_column_mode = ($request->getURIData('mode') === 'column'); + + $src_project = $this->getProject(); $state = $this->getViewState(); $board_uri = $state->newWorkboardURI(); $layout_engine = $state->getLayoutEngine(); - $board_phid = $project->getPHID(); + $board_phid = $src_project->getPHID(); $columns = $layout_engine->getColumns($board_phid); $columns = mpull($columns, null, 'getID'); $column_id = $request->getURIData('columnID'); - $move_column = idx($columns, $column_id); - if (!$move_column) { + $src_column = idx($columns, $column_id); + if (!$src_column) { return new Aphront404Response(); } $move_task_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, - $move_column->getPHID()); + $src_column->getPHID()); $tasks = $state->getObjects(); @@ -50,170 +55,210 @@ final class PhabricatorProjectColumnBulkMoveController ->addCancelButton($board_uri); } - $move_project_phid = $project->getPHID(); - $move_column_phid = null; - $move_project = null; - $move_column = null; - $columns = null; - $errors = array(); - - if ($request->isFormOrHiSecPost()) { - $move_project_phid = head($request->getArr('moveProjectPHID')); - if (!$move_project_phid) { - $move_project_phid = $request->getStr('moveProjectPHID'); - } - - if (!$move_project_phid) { - if ($request->getBool('hasProject')) { - $errors[] = pht('Choose a project to move tasks to.'); - } - } else { - $target_project = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($move_project_phid)) - ->executeOne(); - if (!$target_project) { - $errors[] = pht('You must choose a valid project.'); - } else if (!$project->getHasWorkboard()) { - $errors[] = pht( - 'You must choose a project with a workboard.'); - } else { - $move_project = $target_project; - } - } - - if ($move_project) { - $move_engine = id(new PhabricatorBoardLayoutEngine()) - ->setViewer($viewer) - ->setBoardPHIDs(array($move_project->getPHID())) - ->setFetchAllBoards(true) - ->executeLayout(); - - $columns = $move_engine->getColumns($move_project->getPHID()); - $columns = mpull($columns, null, 'getPHID'); - - foreach ($columns as $key => $column) { - if ($column->isHidden()) { - unset($columns[$key]); - } - } - - $move_column_phid = $request->getStr('moveColumnPHID'); - if (!$move_column_phid) { - if ($request->getBool('hasColumn')) { - $errors[] = pht('Choose a column to move tasks to.'); - } - } else { - if (empty($columns[$move_column_phid])) { - $errors[] = pht( - 'Choose a valid column on the target workboard to move '. - 'tasks to.'); - } else if ($columns[$move_column_phid]->getID() == $column_id) { - $errors[] = pht( - 'You can not move tasks from a column to itself.'); - } else { - $move_column = $columns[$move_column_phid]; + $dst_project_phid = null; + $dst_project = null; + $has_project = false; + if ($is_column_mode) { + $has_project = true; + $dst_project_phid = $src_project->getPHID(); + } else { + if ($request->isFormOrHiSecPost()) { + $has_project = $request->getStr('hasProject'); + if ($has_project) { + // We may read this from a tokenizer input as an array, or from a + // hidden input as a string. + $dst_project_phid = head($request->getArr('dstProjectPHID')); + if (!$dst_project_phid) { + $dst_project_phid = $request->getStr('dstProjectPHID'); } } } } - if ($move_column && $move_project) { - foreach ($move_tasks as $move_task) { - $xactions = array(); + $errors = array(); + $hidden = array(); + + if ($has_project) { + if (!$dst_project_phid) { + $errors[] = pht('Choose a project to move tasks to.'); + } else { + $dst_project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($dst_project_phid)) + ->executeOne(); + if (!$dst_project) { + $errors[] = pht('Choose a valid project to move tasks to.'); + } + + if (!$dst_project->getHasWorkboard()) { + $errors[] = pht('You must choose a project with a workboard.'); + $dst_project = null; + } + } + } + + if ($dst_project) { + $same_project = ($src_project->getID() === $dst_project->getID()); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($dst_project->getPHID())) + ->setFetchAllBoards(true) + ->executeLayout(); + + $dst_columns = $layout_engine->getColumns($dst_project->getPHID()); + $dst_columns = mpull($columns, null, 'getPHID'); + + $has_column = false; + $dst_column = null; + + // If we're performing a move on the same board, default the + // control value to the current column. + if ($same_project) { + $dst_column_phid = $src_column->getPHID(); + } else { + $dst_column_phid = null; + } + + if ($request->isFormOrHiSecPost()) { + $has_column = $request->getStr('hasColumn'); + if ($has_column) { + $dst_column_phid = $request->getStr('dstColumnPHID'); + } + } + + if ($has_column) { + $dst_column = idx($dst_columns, $dst_column_phid); + if (!$dst_column) { + $errors[] = pht('Choose a column to move tasks to.'); + } else { + if ($dst_column->isHidden()) { + $errors[] = pht('You can not move tasks to a hidden column.'); + $dst_column = null; + } else if ($dst_column->getPHID() === $src_column->getPHID()) { + $errors[] = pht('You can not move tasks from a column to itself.'); + $dst_column = null; + } + } + } + + if ($dst_column) { + foreach ($move_tasks as $move_task) { + $xactions = array(); + + // If we're switching projects, get out of the old project first + // and move to the new project. + if (!$same_project) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) + ->setNewValue( + array( + '-' => array( + $src_project->getPHID() => $src_project->getPHID(), + ), + '+' => array( + $dst_project->getPHID() => $dst_project->getPHID(), + ), + )); + } - // If we're switching projects, get out of the old project first - // and move to the new project. - if ($move_project->getID() != $project->getID()) { $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue( - 'edge:type', - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) + ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) ->setNewValue( array( - '-' => array( - $project->getPHID() => $project->getPHID(), - ), - '+' => array( - $move_project->getPHID() => $move_project->getPHID(), + array( + 'columnPHID' => $dst_column->getPHID(), ), )); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setCancelURI($board_uri); + + $editor->applyTransactions($move_task, $xactions); } - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) - ->setNewValue( - array( - array( - 'columnPHID' => $move_column->getPHID(), - ), - )); + // If we did a move on the same workboard, redirect and preserve the + // state parameters. If we moved to a different workboard, go there + // with clean default state. + if ($same_project) { + $done_uri = $board_uri; + } else { + $done_uri = $dst_project->getWorkboardURI(); + } - $editor = id(new ManiphestTransactionEditor()) - ->setActor($viewer) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->setCancelURI($board_uri); - - $editor->applyTransactions($move_task, $xactions); + return id(new AphrontRedirectResponse())->setURI($done_uri); } - return id(new AphrontRedirectResponse()) - ->setURI($board_uri); - } + $title = pht('Move Tasks to Column'); - if ($move_project) { - $column_form = id(new AphrontFormView()) + $form = id(new AphrontFormView()) + ->setViewer($viewer); + + // If we're moving between projects, add a reminder about which project + // you selected in the previous step. + if (!$is_column_mode) { + $form->appendControl( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Project')) + ->setValue($dst_project->getDisplayName())); + } + + $form->appendControl( + id(new AphrontFormSelectControl()) + ->setName('dstColumnPHID') + ->setLabel(pht('Move to Column')) + ->setValue($dst_column_phid) + ->setOptions(mpull($dst_columns, 'getDisplayName', 'getPHID'))); + + $submit = pht('Move Tasks'); + + $hidden['dstProjectPHID'] = $dst_project->getPHID(); + $hidden['hasColumn'] = true; + $hidden['hasProject'] = true; + } else { + $title = pht('Move Tasks to Project'); + + if ($dst_project_phid) { + $dst_project_phid_value = array($dst_project_phid); + } else { + $dst_project_phid_value = array(); + } + + $form = id(new AphrontFormView()) ->setViewer($viewer) ->appendControl( - id(new AphrontFormSelectControl()) - ->setName('moveColumnPHID') - ->setLabel(pht('Move to Column')) - ->setValue($move_column_phid) - ->setOptions(mpull($columns, 'getDisplayName', 'getPHID'))); + id(new AphrontFormTokenizerControl()) + ->setName('dstProjectPHID') + ->setLimit(1) + ->setLabel(pht('Move to Project')) + ->setValue($dst_project_phid_value) + ->setDatasource(new PhabricatorProjectDatasource())); - return $this->newWorkboardDialog() - ->setTitle(pht('Move Tasks')) - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setErrors($errors) - ->addHiddenInput('moveProjectPHID', $move_project->getPHID()) - ->addHiddenInput('hasColumn', true) - ->addHiddenInput('hasProject', true) - ->appendParagraph( - pht( - 'Choose a column on the %s workboard to move tasks to:', - $viewer->renderHandle($move_project->getPHID()))) - ->appendForm($column_form) - ->addSubmitButton(pht('Move Tasks')) - ->addCancelButton($board_uri); + $submit = pht('Continue'); + + $hidden['hasProject'] = true; } - if ($move_project_phid) { - $move_project_phid_value = array($move_project_phid); - } else { - $move_project_phid_value = array(); - } - - $project_form = id(new AphrontFormView()) - ->setViewer($viewer) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setName('moveProjectPHID') - ->setLimit(1) - ->setLabel(pht('Move to Project')) - ->setValue($move_project_phid_value) - ->setDatasource(new PhabricatorProjectDatasource())); - - return $this->newWorkboardDialog() - ->setTitle(pht('Move Tasks')) + $dialog = $this->newWorkboardDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setTitle($title) ->setErrors($errors) - ->addHiddenInput('hasProject', true) - ->appendForm($project_form) - ->addSubmitButton(pht('Continue')) + ->appendForm($form) + ->addSubmitButton($submit) ->addCancelButton($board_uri); + + foreach ($hidden as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; } } From 2de8a23adb09573ebc80d2ff656bef42f1a9adcb Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 09:20:23 -0700 Subject: [PATCH 12/54] Remove remnants of clumsy old URI state handling from workboards Summary: Depends on D20636. Ref T4900. Previously, some workflows didn't know how to identify the default state for the board, so they needed explicit ("force") parameters. Everything uses the same state management code now so we can rip out the old stuff. Test Plan: Changed board filters, selected a custom filter, edited a custom filter. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20637 --- .../PhabricatorProjectBoardViewController.php | 62 ++++++------------- .../state/PhabricatorWorkboardViewState.php | 6 +- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index f49eca1beb..4979f9e78e 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -495,24 +495,26 @@ final class PhabricatorProjectBoardViewController ->setName($name); if ($is_custom) { - $uri = $this->getApplicationURI( - 'board/'.$project->getID().'/filter/query/'.$key.'/'); + // When you're using a custom filter already and you select "Custom + // Filter", you get a dialog back to let you edit the filter. This is + // equivalent to selecting "Advanced Filter..." to configure a new + // filter. + $filter_uri = $state->newWorkboardURI('filter/'); $item->setWorkflow(true); } else { - $uri = $engine->getQueryResultsPageURI($key); + $filter_uri = urisprintf('query/%s/', $key); + $filter_uri = $state->newWorkboardURI($filter_uri); + $filter_uri->removeQueryParam('filter'); } - $uri = $this->getURIWithState($uri) - ->removeQueryParam('filter'); - $item->setHref($uri); + $item->setHref($filter_uri); $items[] = $item; } $id = $project->getID(); - $filter_uri = $this->getApplicationURI("board/{$id}/filter/"); - $filter_uri = $this->getURIWithState($filter_uri, $force = true); + $filter_uri = $state->newWorkboardURI('filter/'); $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') @@ -716,10 +718,8 @@ final class PhabricatorProjectBoardViewController ->setIcon('fa-search') ->setHref($query_uri); - $column_move_uri = $state->newWorkboardURI( - urisprintf( - 'bulkmove/%d/column/', - $column->getID())); + $column_move_uri = urisprintf('bulkmove/%d/column/', $column->getID()); + $column_move_uri = $state->newWorkboardURI($column_move_uri); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-arrows-h') @@ -727,10 +727,8 @@ final class PhabricatorProjectBoardViewController ->setHref($column_move_uri) ->setWorkflow(true); - $project_move_uri = $state->newWorkboardURI( - urisprintf( - 'bulkmove/%d/project/', - $column->getID())); + $project_move_uri = urisprintf('bulkmove/%d/project/', $column->getID()); + $project_move_uri = $state->newWorkboardURI($project_move_uri); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-arrows') @@ -738,10 +736,8 @@ final class PhabricatorProjectBoardViewController ->setHref($project_move_uri) ->setWorkflow(true); - $bulk_edit_uri = $state->newWorkboardURI( - urisprintf( - 'bulk/%d/', - $column->getID())); + $bulk_edit_uri = urisprintf('bulk/%d/', $column->getID()); + $bulk_edit_uri = $state->newWorkboardURI($bulk_edit_uri); $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -767,9 +763,9 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); $can_hide = ($can_edit && !$column->isDefaultColumn()); - $hide_uri = 'board/'.$project->getID().'/hide/'.$column->getID().'/'; - $hide_uri = $this->getApplicationURI($hide_uri); - $hide_uri = $this->getURIWithState($hide_uri); + + $hide_uri = urisprintf('hide/%d/', $column->getID()); + $hide_uri = $state->newWorkboardURI($hide_uri); if (!$column->isHidden()) { $column_items[] = id(new PhabricatorActionView()) @@ -875,26 +871,6 @@ final class PhabricatorProjectBoardViewController return $trigger_button; } - /** - * Add current state parameters (like order and the visibility of hidden - * columns) to a URI. - * - * This allows actions which toggle or adjust one piece of state to keep - * the rest of the board state persistent. If no URI is provided, this method - * starts with the request URI. - * - * @param string|null URI to add state parameters to. - * @param bool True to explicitly include all state. - * @return PhutilURI URI with state parameters. - */ - private function getURIWithState($base = null, $force = false) { - if ($base === null) { - $base = $this->getProject()->getWorkboardURI(); - } - - return $this->getViewState()->newURI($base, $force); - } - private function buildInitializeContent(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $this->getViewer(); diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php index 555ed575a6..5d9d8005b5 100644 --- a/src/applications/project/state/PhabricatorWorkboardViewState.php +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -107,13 +107,13 @@ final class PhabricatorWorkboardViewState return $this->newURI($uri); } - public function newURI($path, $force = false) { + public function newURI($path) { $project = $this->getProject(); $uri = new PhutilURI($path); $request_order = $this->getOrder(); $default_order = $this->getDefaultOrder(); - if ($force || ($request_order !== $default_order)) { + if ($request_order !== $default_order) { $request_value = idx($this->requestState, 'order'); if ($request_value !== null) { $uri->replaceQueryParam('order', $request_value); @@ -126,7 +126,7 @@ final class PhabricatorWorkboardViewState $request_query = $this->getQueryKey(); $default_query = $this->getDefaultQueryKey(); - if ($force || ($request_query !== $default_query)) { + if ($request_query !== $default_query) { $request_value = idx($this->requestState, 'filter'); if ($request_value !== null) { $uri->replaceQueryParam('filter', $request_value); From fc795994f30fcb59839e40d8650af499ec047e5d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 10:19:03 -0700 Subject: [PATCH 13/54] Remove obsolete "options" from workboard "updateCard()" call Summary: Depends on D20637. Ref T4900. This is some ancient dead code that nothing uses. Test Plan: Grepped for `updateCard()` to verify it's private. Searched for "options" and "dirtyColumn" and got no hits. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20638 --- resources/celerity/map.php | 28 +++++++++---------- .../js/application/projects/WorkboardBoard.js | 5 +--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index adc19febba..9a477f6b95 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,7 +412,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'c02a5497', + 'rsrc/js/application/projects/WorkboardBoard.js' => '44f71637', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -743,7 +743,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'c02a5497', + 'javelin-workboard-board' => '44f71637', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1305,6 +1305,18 @@ return array( 'javelin-uri', 'javelin-routable', ), + '44f71637' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '46116c01' => array( 'javelin-request', 'javelin-behavior', @@ -1943,18 +1955,6 @@ return array( 'bde53589' => array( 'phui-inline-comment-view-css', ), - 'c02a5497' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), 'c03f2fb4' => array( 'javelin-install', ), diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 74c0bdf23e..64a5d64b7e 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -570,10 +570,7 @@ JX.install('WorkboardBoard', { list.unlock(); }, - updateCard: function(response, options) { - options = options || {}; - options.dirtyColumns = options.dirtyColumns || {}; - + updateCard: function(response) { var columns = this.getColumns(); var phid = response.objectPHID; From 02315c4c48b961efa1a276bb3b2baea29eb9ba67 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Jul 2019 08:11:53 -0700 Subject: [PATCH 14/54] Fix double-close on dialogs leading to Javascript console error Summary: Ref T13302. The "Close/Cancel" button is currently running two copies of the "dismiss dialog" code, since it's techncally a link with a valid HREF attribute. An alternate formulation of this is perhaps `if (JX.Stratcom.pass()) { return; }` ("let other handlers react to this event; if something kills it, stop processing"), but `pass()` is inherently someone spooky/fragile so try to get away without it. Test Plan: Opened the Javascript console, clicked "Edit Task" on a workboard, clicked "Close" on the dialog. Before: event was double-handled leading to a JS error in the console. After: dialog closes uneventfully. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13302 Differential Revision: https://secure.phabricator.com/D20640 --- resources/celerity/map.php | 28 +++++++++---------- .../rsrc/externals/javelin/lib/Workflow.js | 5 ++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9a477f6b95..775414b101 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' => 'af983028', - 'core.pkg.js' => '5a792749', + 'core.pkg.js' => '73a06a9f', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -253,7 +253,7 @@ return array( 'rsrc/externals/javelin/lib/URI.js' => '2e255291', 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb', 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e', - 'rsrc/externals/javelin/lib/Workflow.js' => '445e21a8', + 'rsrc/externals/javelin/lib/Workflow.js' => '945ff654', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => 'ca686f71', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => '4566e249', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '710377ae', @@ -752,7 +752,7 @@ return array( 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'ebe83a6b', 'javelin-workboard-order-template' => '03e8891f', - 'javelin-workflow' => '445e21a8', + 'javelin-workflow' => '945ff654', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', 'maniphest-task-summary-css' => '61d1667e', @@ -1294,17 +1294,6 @@ return array( '43bc9360' => array( 'javelin-install', ), - '445e21a8' => array( - 'javelin-stratcom', - 'javelin-request', - 'javelin-dom', - 'javelin-vector', - 'javelin-install', - 'javelin-util', - 'javelin-mask', - 'javelin-uri', - 'javelin-routable', - ), '44f71637' => array( 'javelin-install', 'javelin-dom', @@ -1721,6 +1710,17 @@ return array( 'javelin-typeahead-preloaded-source', 'javelin-util', ), + '945ff654' => array( + 'javelin-stratcom', + 'javelin-request', + 'javelin-dom', + 'javelin-vector', + 'javelin-install', + 'javelin-util', + 'javelin-mask', + 'javelin-uri', + 'javelin-routable', + ), '94681e22' => array( 'javelin-magical-init', 'javelin-install', diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js index 3e1bba4a6a..25de547deb 100644 --- a/webroot/rsrc/externals/javelin/lib/Workflow.js +++ b/webroot/rsrc/externals/javelin/lib/Workflow.js @@ -118,6 +118,11 @@ JX.install('Workflow', { return; } + // This link is really a dialog button which we'll handle elsewhere. + if (JX.Stratcom.hasSigil(link, 'jx-workflow-button')) { + return; + } + // Close the dialog. JX.Workflow._pop(); }, From 45f421154179e21b7c352fce1472af609e2d6e66 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jul 2019 11:00:04 -0700 Subject: [PATCH 15/54] Fix URI escaping, which should actually be "%s", not "%p" See . The documentation on `urisprintf()` isn't very clear here, I'll update it in a followup. "%p" is for cases like encoding a branch name (which may contain slashes) as a single path component in a URI. --- .../project/state/PhabricatorWorkboardViewState.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/project/state/PhabricatorWorkboardViewState.php b/src/applications/project/state/PhabricatorWorkboardViewState.php index 5d9d8005b5..04f8498d49 100644 --- a/src/applications/project/state/PhabricatorWorkboardViewState.php +++ b/src/applications/project/state/PhabricatorWorkboardViewState.php @@ -103,7 +103,7 @@ final class PhabricatorWorkboardViewState public function newWorkboardURI($path = null) { $project = $this->getProject(); - $uri = urisprintf('%p%p', $project->getWorkboardURI(), $path); + $uri = urisprintf('%s%s', $project->getWorkboardURI(), $path); return $this->newURI($uri); } From 3c432225251303558de0dae903656d6159f2bf47 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Thu, 4 Jul 2019 16:00:16 -0700 Subject: [PATCH 16/54] Fix paging fatal with flagged objects Summary: Fixes T13331. Just adds a generic `withIDs()` method to `PhabricatorFlagQuery`. Test Plan: Flagged > 100 objects, observed fatal attempting to page. Next page loads as expected after fix. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T13331 Differential Revision: https://secure.phabricator.com/D20642 --- .../flag/query/PhabricatorFlagQuery.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/applications/flag/query/PhabricatorFlagQuery.php b/src/applications/flag/query/PhabricatorFlagQuery.php index 3418f10746..c6c905465d 100644 --- a/src/applications/flag/query/PhabricatorFlagQuery.php +++ b/src/applications/flag/query/PhabricatorFlagQuery.php @@ -6,6 +6,7 @@ final class PhabricatorFlagQuery const GROUP_COLOR = 'color'; const GROUP_NONE = 'none'; + private $ids; private $ownerPHIDs; private $types; private $objectPHIDs; @@ -15,6 +16,11 @@ final class PhabricatorFlagQuery private $needHandles; private $needObjects; + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withOwnerPHIDs(array $owner_phids) { $this->ownerPHIDs = $owner_phids; return $this; @@ -126,6 +132,13 @@ final class PhabricatorFlagQuery protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'flag.id IN (%Ld)', + $this->ids); + } + if ($this->ownerPHIDs) { $where[] = qsprintf( $conn, From 2c435433e0db993e18d00f7fbdde26027c7e3f78 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 10 Jul 2019 09:26:15 -0700 Subject: [PATCH 17/54] Start fleshing out PhabricatorAuthProviderViewController Summary: Ref D20645. Start making this view a little more useful: {F6573605} Test Plan: Mk. 1 eyeball Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20646 --- .../PhabricatorAuthProviderViewController.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php index 532744001c..abf9bf8eff 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php @@ -114,6 +114,86 @@ final class PhabricatorAuthProviderViewController pht('Provider Type'), $config->getProvider()->getProviderName()); + $status = $this->buildStatus($config); + $view->addProperty(pht('Status'), $status); + return $view; } + + private function buildStatus(PhabricatorAuthProviderConfig $config) { + $viewer = $this->getViewer(); + $view = id(new PHUIStatusListView()) + ->setViewer($viewer); + + $icon_enabled = PHUIStatusItemView::ICON_ACCEPT; + $icon_disabled = PHUIStatusItemView::ICON_REJECT; + + $icon_map = array( + true => $icon_enabled, + false => $icon_disabled, + ); + + $color_map = array( + true => 'green', + false => 'red', + ); + + $provider = $config->getProvider(); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getIsEnabled()], + $color_map[$config->getIsEnabled()]) + ->setTarget(pht('Provider Enabled'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowLogin()], + $color_map[$config->getShouldAllowLogin()]) + ->setTarget(pht('Allow Logins'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowRegistration()], + $color_map[$config->getShouldAllowRegistration()]) + ->setTarget(pht('Allow Registration'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowLink()], + $color_map[$config->getShouldAllowLink()]) + ->setTarget(pht('Allow Account Linking'))); + + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAllowUnlink()], + $color_map[$config->getShouldAllowUnlink()]) + ->setTarget(pht('Allow Account Unlinking'))); + + if ($provider->shouldAllowEmailTrustConfiguration()) { + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldTrustEmails()], + $color_map[$config->getShouldTrustEmails()]) + ->setTarget(pht('Trust Email Addresses'))); + } + + if ($provider->supportsAutoLogin()) { + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon( + $icon_map[$config->getShouldAutoLogin()], + $color_map[$config->getShouldAutoLogin()]) + ->setTarget(pht('Allow Auto Login'))); + } + + return $view; + } + } From 099919366b129cb59da5ec4a6f25e117a5a22880 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jul 2019 14:37:33 -0700 Subject: [PATCH 18/54] Fix "add more metadata" fatal in Pholio Summary: Ref T13332. This fix isn't terribly satisfying, but resolves the issue: this behavior may attempt to build HTML blocks with metadata after Javascript footer rendering has started. Use `hsprintf()` to flatten the markup earlier. Test Plan: Put a `T123` reference in the description of a Pholio image, then loaded a mock. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13332 Differential Revision: https://secure.phabricator.com/D20647 --- src/applications/pholio/view/PholioMockImagesView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 786de07cfd..319daa3169 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -103,7 +103,7 @@ final class PholioMockImagesView extends AphrontView { 'width' => $x, 'height' => $y, 'title' => $image->getName(), - 'descriptionMarkup' => $description, + 'descriptionMarkup' => hsprintf('%s', $description), 'isObsolete' => (bool)$image->getIsObsolete(), 'isImage' => $file->isViewableImage(), 'isViewable' => $file->isViewableInBrowser(), From 41ea204144ab18512906f947949608dff2a48e8e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jul 2019 15:47:53 -0700 Subject: [PATCH 19/54] Update one straggling "CAN_INTERACT" check in comment removal Summary: See rPaacc62463d61. D20551 added some `CAN_INTERACT` checks, but `CAN_INTERACT` needs to be checked with `canInteract()` to fall back to `CAN_VIEW` properly. D20558 cleaned up most of this but missed one callsite; fix that up too. Test Plan: Removed a comment on a commit. Reviewers: amckinley, 20after4 Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20648 --- ...bricatorApplicationTransactionCommentRemoveController.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php index f81535e4ae..4d6570b13d 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php @@ -38,10 +38,9 @@ final class PhabricatorApplicationTransactionCommentRemoveController // from locked threads. $object = $xaction->getObject(); - $can_interact = PhabricatorPolicyFilter::hasCapability( + $can_interact = PhabricatorPolicyFilter::canInteract( $viewer, - $object, - PhabricatorPolicyCapability::CAN_INTERACT); + $object); if (!$can_interact && !$viewer->getIsAdmin()) { return $this->newDialog() ->setTitle(pht('Conversation Locked')) From d2935fd7bdc255cd415f640a90f21065a2dba943 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Jul 2019 08:39:35 -0700 Subject: [PATCH 20/54] Fix a bad call to "writeInfo()" in "bin/phd stop" with no PHABRICATOR_INSTANCE defined Summary: See . This call should be `logInfo()`. Test Plan: - Purged `PHABRICATOR_INSTANCE` from my environment. In a Phacility development environment, it comes from loading `services/`. - Ran `bin/phd stop` with all daemons already stopped. - Before: bad call. - After: helpful error. - Ran some other `bin/phd start`, `bin/phd status`, etc., to kick the tires. - Grepped for remaining `writeInfo()` calls (found none). Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20649 --- .../management/PhabricatorDaemonManagementStatusWorkflow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php index 1f7ed951cb..d5af149869 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -22,7 +22,7 @@ final class PhabricatorDaemonManagementStatusWorkflow 'instance ("%s").', $instance)); } else { - $this->writeInfo( + $this->logInfo( pht('NO DAEMONS'), pht('There are no running daemon processes.')); } From 7852adb84bbe20cb90b4f5c79f52bfacf3dca7bd Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Tue, 9 Jul 2019 11:42:53 -0700 Subject: [PATCH 21/54] Actually enforce auth.lock-config Summary: Forgot to post this after D20394. Fixes T7667. Test Plan: * Edited some providers with the config locked and unlocked. * Opened the edit form with the config unlocked, locked the config, then saved, and got a sensible error: {F6576023} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T7667 Differential Revision: https://secure.phabricator.com/D20645 --- .../config/PhabricatorAuthEditController.php | 40 +++++++++++++++++-- .../config/PhabricatorAuthListController.php | 6 ++- .../config/PhabricatorAuthNewController.php | 21 ++++++++++ .../PhabricatorAuthProviderConfigEditor.php | 21 ++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index d3cd2fef98..f602c4fb24 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -79,6 +79,7 @@ final class PhabricatorAuthEditController } $errors = array(); + $validation_exception = null; $v_login = $config->getShouldAllowLogin(); $v_registration = $config->getShouldAllowRegistration(); @@ -153,12 +154,16 @@ final class PhabricatorAuthEditController $editor = id(new PhabricatorAuthProviderConfigEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->applyTransactions($config, $xactions); + ->setContinueOnNoEffect(true); - $next_uri = $config->getURI(); + try { + $editor->applyTransactions($config, $xactions); + $next_uri = $config->getURI(); - return id(new AphrontRedirectResponse())->setURI($next_uri); + return id(new AphrontRedirectResponse())->setURI($next_uri); + } catch (Exception $ex) { + $validation_exception = $ex; + } } } else { $properties = $provider->readFormValuesFromProvider(); @@ -325,12 +330,35 @@ final class PhabricatorAuthEditController $provider->extendEditForm($request, $form, $properties, $issues); + $locked_config_key = 'auth.lock-config'; + $is_locked = PhabricatorEnv::getEnvConfig($locked_config_key); + + $locked_warning = null; + if ($is_locked && !$validation_exception) { + $message = pht( + 'Authentication provider configuration is locked, and can not be '. + 'changed without being unlocked. See the configuration setting %s '. + 'for details.', + phutil_tag( + 'a', + array( + 'href' => '/config/edit/'.$locked_config_key, + ), + $locked_config_key)); + $locked_warning = id(new PHUIInfoView()) + ->setViewer($viewer) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors(array($message)); + } + $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) + ->setDisabled($is_locked) ->setValue($button)); + $help = $provider->getConfigurationHelp(); if ($help) { $form->appendChild(id(new PHUIFormDividerControl())); @@ -346,12 +374,16 @@ final class PhabricatorAuthEditController $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Provider')) ->setFormErrors($errors) + ->setValidationException($validation_exception) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); + + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( + $locked_warning, $form_box, $footer, )); diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index b6ba91e7cd..5d1d85cca6 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -78,12 +78,14 @@ final class PhabricatorAuthListController ->setGuidanceContext($guidance_context) ->newInfoView(); + $is_disabled = (!$can_manage || $is_locked); $button = id(new PHUIButtonView()) ->setTag('a') ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) - ->setHref($this->getApplicationURI('config/new/')) ->setIcon('fa-plus') - ->setDisabled(!$can_manage || $is_locked) + ->setDisabled($is_disabled) + ->setWorkflow($is_disabled) + ->setHref($this->getApplicationURI('config/new/')) ->setText(pht('Add Provider')); $list->setFlush(true); diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index 770c43208d..cb1c537ca8 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -9,6 +9,27 @@ final class PhabricatorAuthNewController $viewer = $this->getViewer(); $cancel_uri = $this->getApplicationURI(); + $locked_config_key = 'auth.lock-config'; + $is_locked = PhabricatorEnv::getEnvConfig($locked_config_key); + + if ($is_locked) { + $message = pht( + 'Authentication provider configuration is locked, and can not be '. + 'changed without being unlocked. See the configuration setting %s '. + 'for details.', + phutil_tag( + 'a', + array( + 'href' => '/config/edit/'.$locked_config_key, + ), + $locked_config_key)); + + return $this->newDialog() + ->setUser($viewer) + ->setTitle(pht('Authentication Config Locked')) + ->appendChild($message) + ->addCancelButton($cancel_uri); + } $providers = PhabricatorAuthProvider::getAllBaseProviders(); diff --git a/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php b/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php index 5599ff5364..1e75edfbf0 100644 --- a/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php +++ b/src/applications/auth/editor/PhabricatorAuthProviderConfigEditor.php @@ -125,4 +125,25 @@ final class PhabricatorAuthProviderConfigEditor return parent::mergeTransactions($u, $v); } + protected function validateAllTransactions( + PhabricatorLiskDAO $object, + array $xactions) { + + $errors = parent::validateAllTransactions($object, $xactions); + + $locked_config_key = 'auth.lock-config'; + $is_locked = PhabricatorEnv::getEnvConfig($locked_config_key); + + if ($is_locked) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + null, + pht('Config Locked'), + pht('Authentication provider configuration is locked, and can not be '. + 'changed without being unlocked.'), + null); + } + + return $errors; + } + } From 2f313a0e0d55076e42e3caa2f0591ff2a0edd2cd Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Mon, 15 Jul 2019 13:59:36 -0700 Subject: [PATCH 22/54] Remove "unstable" status and T2784-specific warning message Summary: Ref T2784. These are lookin' pretty stable. Subclasses like `DiffusionGetLintMessagesConduitAPIMethod` have their warnings about unstable methods, so just remove this warning in the base class. Test Plan: Loaded `/conduit`, observed lack of unstable warnings. Only unstable methods are now `diffusion.getlintmessages`, `diffusion.looksoon`, and `diffusion.updatecoverage`. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T2784 Differential Revision: https://secure.phabricator.com/D20651 --- .../conduit/DiffusionQueryConduitAPIMethod.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php index 716824f9f8..ca32cc0127 100644 --- a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php @@ -7,17 +7,6 @@ abstract class DiffusionQueryConduitAPIMethod return true; } - public function getMethodStatus() { - return self::METHOD_STATUS_UNSTABLE; - } - - public function getMethodStatusDescription() { - return pht( - 'See T2784 - migrating Diffusion working copy calls to conduit methods. '. - 'Until that task is completed (and possibly after) these methods are '. - 'unstable.'); - } - private $diffusionRequest; private $repository; From 97c16997561bb3ca35a509a7e263a46a752cb375 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 17 Jul 2019 12:29:26 -0700 Subject: [PATCH 23/54] Fix transaction title rendering for AuthenticationConfigs Summary: I was poking around in `PhabricatorAuthProviderViewController` and noticed that none of the subclass-specific rendering was working. Figured out that no one ever calls `PhabricatorAuthProviderConfigTransaction->setProvider()`, so instead of adding all those calls, just pull the provider out of the config object. Test Plan: Before: {F6598145} After: {F6598147} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20655 --- .../storage/PhabricatorAuthProviderConfigTransaction.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php index d5a3588d59..f60ba8c734 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php @@ -14,15 +14,8 @@ final class PhabricatorAuthProviderConfigTransaction const PROPERTY_KEY = 'auth:property'; - private $provider; - - public function setProvider(PhabricatorAuthProvider $provider) { - $this->provider = $provider; - return $this; - } - public function getProvider() { - return $this->provider; + return $this->getObject()->getProvider(); } public function getApplicationName() { From db69686927bdf1a68e5e0bd9f62a3779661810f8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jul 2019 10:33:13 -0700 Subject: [PATCH 24/54] Make pressing "R" on your keyboard reload the card state on workboards Summary: Depends on D20638. Ref T4900. This is an incremental step toward proper workboard updates. Currently, the client can mostly update its view because we do updates when you edit or move a card, and the client and server know how to send lists of card updates, so a lot of the work is already done. However, the code assumes we're only updating/redrawing one card at a time. Make the client accept and process multiple card updates. In future changes, I'll add versioning (so we only update cards that have actually changed), fix the "TODO" around ordering, and move toward actual Aphlict-based real-time updates. Test Plan: - Opened the same workboard in two windows. - Edited cards in one window, pressed "R" (capital letter, with no modifier keys) to reload the second window. - Saw edits and moves reflected accurately after sync, except for some special cases of header/order interaction (see "TODO"). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20639 --- resources/celerity/map.php | 76 ++++----- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectApplication.php | 2 + ...habricatorProjectBoardReloadController.php | 38 +++++ .../PhabricatorProjectBoardViewController.php | 1 + .../engine/PhabricatorBoardResponseEngine.php | 143 +++++++++++------ .../js/application/projects/WorkboardBoard.js | 148 ++++++++++++------ .../projects/WorkboardController.js | 1 + .../projects/behavior-project-boards.js | 1 + 9 files changed, 283 insertions(+), 129 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectBoardReloadController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 775414b101..87c5ad5b85 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,16 +412,16 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '44f71637', + 'rsrc/js/application/projects/WorkboardBoard.js' => '34c2f539', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', - 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', + 'rsrc/js/application/projects/WorkboardController.js' => 'b9d0c2f3', 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445', + 'rsrc/js/application/projects/behavior-project-boards.js' => '58cb6a88', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -667,7 +667,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'aad45445', + 'javelin-behavior-project-boards' => '58cb6a88', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -743,11 +743,11 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '44f71637', + 'javelin-workboard-board' => '34c2f539', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', - 'javelin-workboard-controller' => '42c7a5a7', + 'javelin-workboard-controller' => 'b9d0c2f3', 'javelin-workboard-drop-effect' => '8e0aa661', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'ebe83a6b', @@ -1202,6 +1202,18 @@ return array( 'javelin-install', 'javelin-util', ), + '34c2f539' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '34c53422' => array( 'javelin-behavior', 'javelin-dom', @@ -1264,16 +1276,6 @@ return array( '4234f572' => array( 'syntax-default-css', ), - '42c7a5a7' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-drag-and-drop-file-upload', - 'javelin-workboard-board', - ), '4370900d' => array( 'javelin-install', 'javelin-util', @@ -1294,18 +1296,6 @@ return array( '43bc9360' => array( 'javelin-install', ), - '44f71637' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '46116c01' => array( 'javelin-request', 'javelin-behavior', @@ -1424,6 +1414,16 @@ return array( 'javelin-vector', 'javelin-typeahead-static-source', ), + '58cb6a88' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), '5902260c' => array( 'javelin-util', 'javelin-magical-init', @@ -1852,16 +1852,6 @@ return array( 'javelin-dom', 'javelin-util', ), - 'aad45445' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', - ), 'ab85e184' => array( 'javelin-install', 'javelin-dom', @@ -1952,6 +1942,16 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'b9d0c2f3' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', + ), 'bde53589' => array( 'phui-inline-comment-view-css', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 963666fe82..095e7231fb 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4163,6 +4163,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardFilterController' => 'applications/project/controller/PhabricatorProjectBoardFilterController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardManageController' => 'applications/project/controller/PhabricatorProjectBoardManageController.php', + 'PhabricatorProjectBoardReloadController' => 'applications/project/controller/PhabricatorProjectBoardReloadController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php', @@ -10431,6 +10432,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardFilterController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardManageController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectBoardReloadController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 1c04e0c2f9..af4a06fcc4 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -99,6 +99,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardDefaultController', 'filter/(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardFilterController', + 'reload/' + => 'PhabricatorProjectBoardReloadController', ), 'column/' => array( 'remove/(?P\d+)/' => diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php new file mode 100644 index 0000000000..43752e0cd5 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -0,0 +1,38 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $state = $this->getViewState(); + $board_uri = $state->newWorkboardURI(); + + $layout_engine = $state->getLayoutEngine(); + + $board_phid = $project->getPHID(); + + $objects = $state->getObjects(); + $object_phids = mpull($objects, 'getPHID'); + + $engine = id(new PhabricatorBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setUpdatePHIDs($object_phids); + + // TODO: We don't currently process "order" properly. If a user is viewing + // a board grouped by "Owner", and another user changes a task to be owned + // by a user who currently owns nothing on the board, the new header won't + // generate correctly if the first user presses "R". + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 4979f9e78e..13a75c5a73 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -286,6 +286,7 @@ final class PhabricatorProjectBoardViewController 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', 'coverURI' => $this->getApplicationURI('cover/'), + 'reloadURI' => phutil_string_cast($state->newWorkboardURI('reload/')), 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index f22254e43a..dbd2e31a3d 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -6,6 +6,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $boardPHID; private $objectPHID; private $visiblePHIDs; + private $updatePHIDs = array(); private $ordering; private $sounds; @@ -45,6 +46,15 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->visiblePHIDs; } + public function setUpdatePHIDs(array $update_phids) { + $this->updatePHIDs = $update_phids; + return $this; + } + + public function getUpdatePHIDs() { + return $this->updatePHIDs; + } + public function setOrdering(PhabricatorProjectColumnOrder $ordering) { $this->ordering = $ordering; return $this; @@ -71,36 +81,41 @@ final class PhabricatorBoardResponseEngine extends Phobject { // Load all the other tasks that are visible in the affected columns and // perform layout for them. - $visible_phids = $this->getAllVisiblePHIDs(); + $all_phids = $this->getAllVisiblePHIDs(); $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs($visible_phids) + ->setObjectPHIDs($all_phids) ->executeLayout(); - $object_columns = $layout_engine->getObjectColumns( - $board_phid, - $object_phid); - $natural = array(); - foreach ($object_columns as $column_phid => $column) { + + $update_phids = $this->getAllUpdatePHIDs(); + $update_columns = array(); + foreach ($update_phids as $update_phid) { + $update_columns += $layout_engine->getObjectColumns( + $board_phid, + $update_phid); + } + + foreach ($update_columns as $column_phid => $column) { $column_object_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $column_phid); $natural[$column_phid] = array_values($column_object_phids); } - $all_visible = id(new ManiphestTaskQuery()) + $all_objects = id(new ManiphestTaskQuery()) ->setViewer($viewer) - ->withPHIDs($visible_phids) + ->withPHIDs($all_phids) ->execute(); - $all_visible = mpull($all_visible, null, 'getPHID'); + $all_objects = mpull($all_objects, null, 'getPHID'); if ($ordering) { - $vectors = $ordering->getSortVectorsForObjects($all_visible); - $header_keys = $ordering->getHeaderKeysForObjects($all_visible); - $headers = $ordering->getHeadersForObjects($all_visible); + $vectors = $ordering->getSortVectorsForObjects($all_objects); + $header_keys = $ordering->getHeaderKeysForObjects($all_objects); + $headers = $ordering->getHeadersForObjects($all_objects); $headers = mpull($headers, 'toDictionary'); } else { $vectors = array(); @@ -108,19 +123,10 @@ final class PhabricatorBoardResponseEngine extends Phobject { $headers = array(); } - $object = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); - } - - $template = $this->buildTemplate($object); + $templates = $this->newCardTemplates(); $cards = array(); - foreach ($all_visible as $card_phid => $object) { + foreach ($all_objects as $card_phid => $object) { $card = array( 'vectors' => array(), 'headers' => array(), @@ -144,8 +150,11 @@ final class PhabricatorBoardResponseEngine extends Phobject { $card['properties'] = self::newTaskProperties($object); } - if ($card_phid === $object_phid) { - $card['nodeHTMLTemplate'] = hsprintf('%s', $template); + if (isset($templates[$card_phid])) { + $card['nodeHTMLTemplate'] = hsprintf('%s', $templates[$card_phid]); + $card['update'] = true; + } else { + $card['update'] = false; } $card['vectors'] = (object)$card['vectors']; @@ -156,7 +165,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { } $payload = array( - 'objectPHID' => $object_phid, 'columnMaps' => $natural, 'cards' => $cards, 'headers' => $headers, @@ -176,22 +184,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { ); } - private function buildTemplate($object) { - $viewer = $this->getViewer(); - $object_phid = $this->getObjectPHID(); - - $excluded_phids = $this->loadExcludedProjectPHIDs(); - - $rendering_engine = id(new PhabricatorBoardRenderingEngine()) - ->setViewer($viewer) - ->setObjects(array($object)) - ->setExcludedProjectPHIDs($excluded_phids); - - $card = $rendering_engine->renderCard($object_phid); - - return hsprintf('%s', $card->getItem()); - } - private function loadExcludedProjectPHIDs() { $viewer = $this->getViewer(); $board_phid = $this->getBoardPHID(); @@ -211,10 +203,67 @@ final class PhabricatorBoardResponseEngine extends Phobject { } private function getAllVisiblePHIDs() { - $visible_phids = $this->getVisiblePHIDs(); - $visible_phids[] = $this->getObjectPHID(); - $visible_phids = array_fuse($visible_phids); - return $visible_phids; + $phids = $this->getAllUpdatePHIDs(); + + foreach ($this->getVisiblePHIDs() as $phid) { + $phids[] = $phid; + } + + $phids = array_fuse($phids); + + return $phids; + } + + private function getAllUpdatePHIDs() { + $phids = $this->getUpdatePHIDs(); + + $object_phid = $this->getObjectPHID(); + if ($object_phid) { + $phids[] = $object_phid; + } + + $phids = array_fuse($phids); + + return $phids; + } + + private function newCardTemplates() { + $viewer = $this->getViewer(); + + $update_phids = $this->getAllUpdatePHIDs(); + if (!$update_phids) { + return array(); + } + + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($update_phids) + ->needProjectPHIDs(true) + ->execute(); + + if (!$objects) { + return array(); + } + + $excluded_phids = $this->loadExcludedProjectPHIDs(); + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects($objects) + ->setExcludedProjectPHIDs($excluded_phids); + + $templates = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $card = $rendering_engine->renderCard($object_phid); + $item = $card->getItem(); + $template = hsprintf('%s', $item); + + $templates[$object_phid] = $template; + } + + return $templates; } } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 64a5d64b7e..ba015f592d 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -129,6 +129,13 @@ JX.install('WorkboardBoard', { start: function() { this._setupDragHandlers(); + // TODO: This is temporary code to make it easier to debug this workflow + // by pressing the "R" key. + var on_reload = JX.bind(this, this._reloadCards); + new JX.KeyboardShortcut('R', 'Reload Card State (Prototype)') + .setHandler(on_reload) + .register(); + for (var k in this._columns) { this._columns[k].redraw(); } @@ -551,15 +558,6 @@ JX.install('WorkboardBoard', { }, _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { - var src_column = this.getColumn(src_phid); - var dst_column = this.getColumn(dst_phid); - - var card = src_column.removeCard(response.objectPHID); - dst_column.addCard(card, after_phid); - - src_column.markForRedraw(); - dst_column.markForRedraw(); - this.updateCard(response); var sounds = response.sounds || []; @@ -572,37 +570,51 @@ JX.install('WorkboardBoard', { updateCard: function(response) { var columns = this.getColumns(); + var column_phid; + var card_phid; + var card_data; - var phid = response.objectPHID; + // The server may send us a full or partial update for a card. If we've + // received a full update, we're going to redraw the entire card and may + // need to change which columns it appears in. - for (var add_phid in response.columnMaps) { - var target_column = this.getColumn(add_phid); + // For a partial update, we've just received supplemental sorting or + // property information and do not need to perform a full redraw. + + // When we reload card state, edit a card, or move a card, we get a full + // update for the card. + + // Ween we move a card in a column, we may get a partial update for other + // visible cards in the column. + + + // Figure out which columns each card now appears in. For cards that + // have received a full update, we'll use this map to move them into + // the correct columns. + var update_map = {}; + for (column_phid in response.columnMaps) { + var target_column = this.getColumn(column_phid); if (!target_column) { // If the column isn't visible, don't try to add a card to it. continue; } - target_column.newCard(phid); - } + var column_map = response.columnMaps[column_phid]; - var column_maps = response.columnMaps; - var natural_column; - for (var natural_phid in column_maps) { - natural_column = this.getColumn(natural_phid); - if (!natural_column) { - // Our view of the board may be out of date, so we might get back - // information about columns that aren't visible. Just ignore the - // position information for any columns we aren't displaying on the - // client. - continue; + for (var ii = 0; ii < column_map.length; ii++) { + card_phid = column_map[ii]; + if (!update_map[card_phid]) { + update_map[card_phid] = {}; + } + update_map[card_phid][column_phid] = true; } - - natural_column.setNaturalOrder(column_maps[natural_phid]); } - for (var card_phid in response.cards) { - var card_data = response.cards[card_phid]; + // Process partial updates for cards. This is supplemental data which + // we can just merge in without any special handling. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; var card_template = this.getCardTemplate(card_phid); if (card_data.nodeHTMLTemplate) { @@ -623,6 +635,57 @@ JX.install('WorkboardBoard', { } } + + // Process full updates for cards which we have a full update for. This + // may involve moving them between columns. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.update) { + continue; + } + + for (column_phid in columns) { + var column = columns[column_phid]; + var card = column.getCard(card_phid); + + if (card) { + card.redraw(); + column.markForRedraw(); + } + + // Compare the server state to the client state, and add or remove + // cards on the client as necessary to synchronize them. + + if (update_map[card_phid][column_phid]) { + if (!card) { + column.newCard(card_phid); + column.markForRedraw(); + } + } else { + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } + } + } + + var column_maps = response.columnMaps; + var natural_column; + for (var natural_phid in column_maps) { + natural_column = this.getColumn(natural_phid); + if (!natural_column) { + // Our view of the board may be out of date, so we might get back + // information about columns that aren't visible. Just ignore the + // position information for any columns we aren't displaying on the + // client. + continue; + } + + natural_column.setNaturalOrder(column_maps[natural_phid]); + } + var headers = response.headers; for (var jj = 0; jj < headers.length; jj++) { var header = headers[jj]; @@ -634,22 +697,6 @@ JX.install('WorkboardBoard', { .setEditProperties(header.editProperties); } - for (var column_phid in columns) { - var column = columns[column_phid]; - - var cards = column.getCards(); - for (var object_phid in cards) { - if (object_phid !== phid) { - continue; - } - - var card = cards[object_phid]; - card.redraw(); - - column.markForRedraw(); - } - } - this._redrawColumns(); }, @@ -660,6 +707,19 @@ JX.install('WorkboardBoard', { columns[k].redraw(); } } + }, + + _reloadCards: function() { + var data = {}; + var on_reload = JX.bind(this, this._onReloadResponse); + + new JX.Request(this.getController().getReloadURI(), on_reload) + .setData(data) + .send(); + }, + + _onReloadResponse: function(response) { + this.updateCard(response); } } diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js index 8fe88eb50c..da5d177bb9 100644 --- a/webroot/rsrc/js/application/projects/WorkboardController.js +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -21,6 +21,7 @@ JX.install('WorkboardController', { uploadURI: null, coverURI: null, moveURI: null, + reloadURI: null, chunkThreshold: null }, diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index bba6db7a49..26e5d90f8e 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -71,6 +71,7 @@ JX.behavior('project-boards', function(config, statics) { .setUploadURI(config.uploadURI) .setCoverURI(config.coverURI) .setMoveURI(config.moveURI) + .setReloadURI(config.reloadURI) .setChunkThreshold(config.chunkThreshold) .start(); } From 1ee6ecf397680eeaa3bec4ce05c3a45566622238 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 17 Jul 2019 10:12:39 -0700 Subject: [PATCH 25/54] Move "BoardResponseEngine" toward a more comprehensive update model Summary: Depends on D20639. Ref T4900. Currently, "BoardResponseEngine" has a `setObjectPHID()` method. This is called after edit operations to mean "we just edited object X, so we know it needs to be updated". Move toward `setUpdatePHIDs(...)` in all cases, with `setUpdatePHIDs(array(the-object-we-just-edited))` as a special case of that. After this change, callers pass: - An optional list of PHIDs they know need to be updated on the client. Today, this is always be a card we just edited (on edit/move flows), or a sort of made-up list of PHIDs for the moment (when you press "R"). In the future, the "R" endpoint will do a better job of figuring out a more realistic update set. - An optional list of PHIDs currently visible on the client. This is used to update ordering details and mark cards for removal. This is currently passed by edit/move, but not by pressing "R" (it will be in the future). - An optional list of objects. The "R" workflow has to load these anyway, so we can save a couple queries by letting callers pass them. For now, the edit/move flows still rely on the engine to figure out what it needs to load. This does very little to actually change client behavior, it mostly just paves the way for the next update to the "R" workflow to make it handle add/remove cases properly. Test Plan: - Edited and moved cards on a workboard. - Pressed "R" to reload a workboard. Neither of these operations seem any worse off than they were before. They still don't fully work: - When you edit a card and delete the current workboard project from it, it remains visible. This is also the behavior on `master`. This is sort of intentional since we don't necessarily want to make these cards suddenly disappear? Ideally, we would probably have some kind of "tombstone" state where the card can still be edited but can't be dragged, and the next explicit user interaction would clean up old tombstones. This interaction is very rare and I don't think it's particularly important to specialize. - When a card is removed from the board, "R" can't currently figure out that it should be removed from the client. This is because the client does not yet pass a "visiblePHIDs" state. It will in an upcoming change. - The "R" flow always sends a full set of card updates, and can not yet detect that some cards have not changed. - There's a TODO, but some ordering stuff isn't handled yet. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20652 --- resources/celerity/map.php | 28 +++--- .../maniphest/editor/ManiphestEditEngine.php | 2 +- ...habricatorProjectBoardReloadController.php | 1 + .../PhabricatorProjectController.php | 2 +- .../engine/PhabricatorBoardResponseEngine.php | 95 ++++++++++--------- .../js/application/projects/WorkboardBoard.js | 26 ++++- 6 files changed, 91 insertions(+), 63 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 87c5ad5b85..1de5ab7f61 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,7 +412,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '34c2f539', + 'rsrc/js/application/projects/WorkboardBoard.js' => '46573d65', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -743,7 +743,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '34c2f539', + 'javelin-workboard-board' => '46573d65', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1202,18 +1202,6 @@ return array( 'javelin-install', 'javelin-util', ), - '34c2f539' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '34c53422' => array( 'javelin-behavior', 'javelin-dom', @@ -1304,6 +1292,18 @@ return array( 'javelin-util', 'phabricator-busy', ), + '46573d65' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '47a0728b' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 2a8730d5c6..7cb788e199 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -434,7 +434,7 @@ EODOCS $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) - ->setObjectPHID($object_phid) + ->setUpdatePHIDs(array($object_phid)) ->setVisiblePHIDs($visible_phids); if ($ordering) { diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php index 43752e0cd5..b3c4e06a05 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -25,6 +25,7 @@ final class PhabricatorProjectBoardReloadController $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) + ->setObjects($objects) ->setUpdatePHIDs($object_phids); // TODO: We don't currently process "order" properly. If a user is viewing diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 4391a61ff3..14b4aa151e 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -184,7 +184,7 @@ abstract class PhabricatorProjectController extends PhabricatorController { $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) - ->setObjectPHID($object_phid) + ->setUpdatePHIDs(array($object_phid)) ->setVisiblePHIDs($visible_phids) ->setSounds($sounds); diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index dbd2e31a3d..96a6d8c457 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -3,9 +3,9 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $viewer; + private $objects; private $boardPHID; - private $objectPHID; - private $visiblePHIDs; + private $visiblePHIDs = array(); private $updatePHIDs = array(); private $ordering; private $sounds; @@ -28,13 +28,13 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->boardPHID; } - public function setObjectPHID($object_phid) { - $this->objectPHID = $object_phid; + public function setObjects(array $objects) { + $this->objects = $objects; return $this; } - public function getObjectPHID() { - return $this->objectPHID; + public function getObjects() { + return $this->objects; } public function setVisiblePHIDs(array $visible_phids) { @@ -75,13 +75,30 @@ final class PhabricatorBoardResponseEngine extends Phobject { public function buildResponse() { $viewer = $this->getViewer(); - $object_phid = $this->getObjectPHID(); $board_phid = $this->getBoardPHID(); $ordering = $this->getOrdering(); + $update_phids = $this->getUpdatePHIDs(); + $update_phids = array_fuse($update_phids); + + $visible_phids = $this->getVisiblePHIDs(); + $visible_phids = array_fuse($visible_phids); + + $all_phids = $update_phids + $visible_phids; + // Load all the other tasks that are visible in the affected columns and // perform layout for them. - $all_phids = $this->getAllVisiblePHIDs(); + + if ($this->objects !== null) { + $all_objects = $this->getObjects(); + $all_objects = mpull($all_objects, null, 'getPHID'); + } else { + $all_objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($all_phids) + ->execute(); + $all_objects = mpull($all_objects, null, 'getPHID'); + } $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) @@ -91,7 +108,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { $natural = array(); - $update_phids = $this->getAllUpdatePHIDs(); $update_columns = array(); foreach ($update_phids as $update_phid) { $update_columns += $layout_engine->getObjectColumns( @@ -106,12 +122,6 @@ final class PhabricatorBoardResponseEngine extends Phobject { $natural[$column_phid] = array_values($column_object_phids); } - $all_objects = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($all_phids) - ->execute(); - $all_objects = mpull($all_objects, null, 'getPHID'); - if ($ordering) { $vectors = $ordering->getSortVectorsForObjects($all_objects); $header_keys = $ordering->getHeaderKeysForObjects($all_objects); @@ -164,6 +174,17 @@ final class PhabricatorBoardResponseEngine extends Phobject { $cards[$card_phid] = $card; } + // Mark cards which are currently visible on the client but not visible + // on the board on the server for removal from the client view of the + // board state. + foreach ($visible_phids as $card_phid) { + if (!isset($cards[$card_phid])) { + $cards[$card_phid] = array( + 'remove' => true, + ); + } + } + $payload = array( 'columnMaps' => $natural, 'cards' => $cards, @@ -202,44 +223,26 @@ final class PhabricatorBoardResponseEngine extends Phobject { return array_fuse($exclude_phids); } - private function getAllVisiblePHIDs() { - $phids = $this->getAllUpdatePHIDs(); - - foreach ($this->getVisiblePHIDs() as $phid) { - $phids[] = $phid; - } - - $phids = array_fuse($phids); - - return $phids; - } - - private function getAllUpdatePHIDs() { - $phids = $this->getUpdatePHIDs(); - - $object_phid = $this->getObjectPHID(); - if ($object_phid) { - $phids[] = $object_phid; - } - - $phids = array_fuse($phids); - - return $phids; - } - private function newCardTemplates() { $viewer = $this->getViewer(); - $update_phids = $this->getAllUpdatePHIDs(); + $update_phids = $this->getUpdatePHIDs(); if (!$update_phids) { return array(); } + $update_phids = array_fuse($update_phids); - $objects = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($update_phids) - ->needProjectPHIDs(true) - ->execute(); + if ($this->objects === null) { + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($update_phids) + ->needProjectPHIDs(true) + ->execute(); + } else { + $objects = $this->getObjects(); + $objects = mpull($objects, null, 'getPHID'); + $objects = array_select_keys($objects, $update_phids); + } if (!$objects) { return array(); diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index ba015f592d..f1a9331de5 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -611,10 +611,35 @@ JX.install('WorkboardBoard', { } } + // Process card removals. These are cases where the client still sees + // a particular card on a board but it has been removed on the server. + for (card_phid in response.cards) { + card_data = response.cards[card_phid]; + + if (!card_data.remove) { + continue; + } + + for (column_phid in columns) { + var column = columns[column_phid]; + + var card = column.getCard(card_phid); + if (card) { + column.removeCard(card_phid); + column.markForRedraw(); + } + } + } + // Process partial updates for cards. This is supplemental data which // we can just merge in without any special handling. for (card_phid in response.cards) { card_data = response.cards[card_phid]; + + if (card_data.remove) { + continue; + } + var card_template = this.getCardTemplate(card_phid); if (card_data.nodeHTMLTemplate) { @@ -635,7 +660,6 @@ JX.install('WorkboardBoard', { } } - // Process full updates for cards which we have a full update for. This // may involve moving them between columns. for (card_phid in response.cards) { From 8669c3c0d226120fdd8506f416f846957f2aba96 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 3 Jul 2019 12:36:17 -0700 Subject: [PATCH 26/54] When updating a workboard with "R", send the client visible set with version numbers Summary: Depends on D20652. Ref T4900. When the user presses "R", send a list of cards currently visible on the client and their version numbers. On the server: - Compare the client verisons to the server versions so we can skip updates for objects which have not changed. (For now, the client version is always "1" and the server version is always "2", so this doesn't do anything meaningful, and every card is always updated.) - Compare the client visible set to the server visible set and "remove" any cards which have been removed from the board. I believe this means that "R" always puts the board into the right state (except for some issues with client orderings not being fully handled yet). It's not tremendously efficient, but we can make versioning better (using the largest object transaction ID) to improve that and loading the page in the first place doesn't take all that long so even sending down the full visible set shouldn't be a huge problem. Test Plan: - In window A, removed a card from a board. - In window B, pressed "R" and saw the removal reflected on the client. - (Also added cards, edited cards, etc., and didn't catch anything exploding.) Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20653 --- resources/celerity/map.php | 38 +++++++++---------- ...habricatorProjectBoardReloadController.php | 33 +++++++++++++++- .../js/application/projects/WorkboardBoard.js | 15 +++++++- .../projects/WorkboardCardTemplate.js | 5 +++ 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 1de5ab7f61..675cdebaac 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,9 +412,9 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '46573d65', + 'rsrc/js/application/projects/WorkboardBoard.js' => '50147a89', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', - 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', + 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '84f82dad', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', 'rsrc/js/application/projects/WorkboardController.js' => 'b9d0c2f3', 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661', @@ -743,9 +743,9 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '46573d65', + 'javelin-workboard-board' => '50147a89', 'javelin-workboard-card' => '0392a5d8', - 'javelin-workboard-card-template' => '2a61f8d4', + 'javelin-workboard-card-template' => '84f82dad', 'javelin-workboard-column' => 'c3d24e63', 'javelin-workboard-controller' => 'b9d0c2f3', 'javelin-workboard-drop-effect' => '8e0aa661', @@ -1133,9 +1133,6 @@ return array( 'javelin-stratcom', 'javelin-behavior', ), - '2a61f8d4' => array( - 'javelin-install', - ), '2a8b62d9' => array( 'multirow-row-manager', 'javelin-install', @@ -1292,18 +1289,6 @@ return array( 'javelin-util', 'phabricator-busy', ), - '46573d65' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '47a0728b' => array( 'javelin-behavior', 'javelin-dom', @@ -1372,6 +1357,18 @@ return array( '4feea7d3' => array( 'trigger-rule-control', ), + '50147a89' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '506aa3f4' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1619,6 +1616,9 @@ return array( 'javelin-resource', 'javelin-routable', ), + '84f82dad' => array( + 'javelin-install', + ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php index b3c4e06a05..e029cba896 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -20,13 +20,42 @@ final class PhabricatorProjectBoardReloadController $board_phid = $project->getPHID(); $objects = $state->getObjects(); - $object_phids = mpull($objects, 'getPHID'); + $objects = mpull($objects, null, 'getPHID'); + + try { + $client_state = $request->getStr('state'); + $client_state = phutil_json_decode($client_state); + } catch (PhutilJSONParserException $ex) { + $client_state = array(); + } + + // Figure out which objects need to be updated: either the client has an + // out-of-date version of them (objects which have been edited); or they + // exist on the client but not on the server (objects which have been + // removed from the board); or they exist on the server but not on the + // client (objects which have been added to the board). + + $update_objects = array(); + foreach ($objects as $object_phid => $object) { + + // TODO: For now, this is always hard-coded. + $object_version = 2; + + $client_version = idx($client_state, $object_phid, 0); + if ($object_version > $client_version) { + $update_objects[$object_phid] = $object; + } + } + + $update_phids = array_keys($update_objects); + $visible_phids = array_keys($client_state); $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) ->setObjects($objects) - ->setUpdatePHIDs($object_phids); + ->setUpdatePHIDs($update_phids) + ->setVisiblePHIDs($visible_phids); // TODO: We don't currently process "order" properly. If a user is viewing // a board grouped by "Owner", and another user changes a task to be owned diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index f1a9331de5..a6fc97beb4 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -734,7 +734,20 @@ JX.install('WorkboardBoard', { }, _reloadCards: function() { - var data = {}; + var state = {}; + + var columns = this.getColumns(); + for (var column_phid in columns) { + var cards = columns[column_phid].getCards(); + for (var card_phid in cards) { + state[card_phid] = this.getCardTemplate(card_phid).getVersion(); + } + } + + var data = { + state: JX.JSON.stringify(state), + }; + var on_reload = JX.bind(this, this._onReloadResponse); new JX.Request(this.getController().getReloadURI(), on_reload) diff --git a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js index 58f3f9e97f..e3387a4e18 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js +++ b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js @@ -28,6 +28,11 @@ JX.install('WorkboardCardTemplate', { return this._phid; }, + getVersion: function() { + // TODO: For now, just return a constant version number. + return 1; + }, + setNodeHTMLTemplate: function(html) { this._html = html; return this; From d02beaf8161a15e3babaef5ef99f433bf260cd02 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 17 Jul 2019 10:55:17 -0700 Subject: [PATCH 27/54] Make reloading workboards with "R" respect workboard ordering Summary: Depends on D20653. Ref T4900. Pass ordering details to the reload endpoint so it can give the client accurate ordering/header information in the response. The removed comment mentions this, but here's why this is a difficult mess: - In window A, view a board with "Group by: Owner" and no tasks owned by "Alice". Since "Alice" owns no tasks, this means the columns do not have an "Assigned to: Alice" header! - In window B, edit task T and assign it to Alice. - In window A, press "R". Window A now not only needs to update to properly reflect the state of task T, it actually needs to draw a new "Assigned to: Alice" header in every column. Fortunately, the "group by" code anticipates this being a big mess, is fairly careful about handling it, and the client can handle this state change and the actual code change here isn't too involved. This is just causing a lot of not-very-obvious indirect effects in the pipeline to handle these situations that need complex redraws. Test Plan: - After making various normal edits/creates/moves in window A, pressed "R" in window B. Saw ordering reflected correctly after sync. - Went through the whole "Group by: Owner" + assign to unrepresented owner flow above. After pressing "R", saw "Assigned to: Alice" appear on the board. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20654 --- resources/celerity/map.php | 28 +++++++++---------- ...habricatorProjectBoardReloadController.php | 15 ++++++---- .../js/application/projects/WorkboardBoard.js | 1 + 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 675cdebaac..febcddca82 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,7 +412,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '50147a89', + 'rsrc/js/application/projects/WorkboardBoard.js' => '19df903f', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '84f82dad', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -743,7 +743,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '50147a89', + 'javelin-workboard-board' => '19df903f', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '84f82dad', 'javelin-workboard-column' => 'c3d24e63', @@ -1030,6 +1030,18 @@ return array( '17b71bbc' => array( 'phui-theme-css', ), + '19df903f' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '1b6acc2a' => array( 'javelin-magical-init', 'javelin-util', @@ -1357,18 +1369,6 @@ return array( '4feea7d3' => array( 'trigger-rule-control', ), - '50147a89' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '506aa3f4' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php index e029cba896..6204671505 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardReloadController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardReloadController.php @@ -11,6 +11,15 @@ final class PhabricatorProjectBoardReloadController return $response; } + $order = $request->getStr('order'); + if (!strlen($order)) { + $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY; + } + + $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); + $project = $this->getProject(); $state = $this->getViewState(); $board_uri = $state->newWorkboardURI(); @@ -53,15 +62,11 @@ final class PhabricatorProjectBoardReloadController $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) + ->setOrdering($ordering) ->setObjects($objects) ->setUpdatePHIDs($update_phids) ->setVisiblePHIDs($visible_phids); - // TODO: We don't currently process "order" properly. If a user is viewing - // a board grouped by "Owner", and another user changes a task to be owned - // by a user who currently owns nothing on the board, the new header won't - // generate correctly if the first user presses "R". - return $engine->buildResponse(); } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index a6fc97beb4..c78fcd4eaf 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -746,6 +746,7 @@ JX.install('WorkboardBoard', { var data = { state: JX.JSON.stringify(state), + order: this.getOrder() }; var on_reload = JX.bind(this, this._onReloadResponse); From 9ab5f59ca23c146508557c88df0cd85ea8cc34f4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 17 Jul 2019 15:33:02 -0700 Subject: [PATCH 28/54] Export "date" and "remarkup" custom fields to Excel + "zip" extension check Summary: Fixes T13342. This does a few different things, although all of them seem small enough that I didn't bother splitting it up: - Support export of "remarkup" custom fields as text. There's some argument here to export them in some kind of structure if the target is JSON, but it's hard for me to really imagine we'll live in a world some day where we really regret just exporting them as text. - Support export of "date" custom fields as dates. This is easy except that I added `null` support. - If you built PHP from source without "--enable-zip", as I did, you can hit the TODO in Excel exports about "ZipArchive". Since I had a reproduction case, test for "ZipArchive" and give the user a better error if it's missing. - Add a setup check for the "zip" extension to try to avoid getting there in the first place. This is normally part of PHP so I believe users generally won't hit it, I just hit it because I built from source. See also T13232. Test Plan: - Added a custom "date" field. On tasks A and B, set it to null and some non-null value. Exported both tasks to Excel/JSON/text, saw null and a date, respectively. - Added a custom "remarkup" field, exported some values, saw the values in Excel. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13342 Differential Revision: https://secure.phabricator.com/D20658 --- src/__phutil_library_map__.php | 2 ++ .../config/check/PhabricatorZipSetupCheck.php | 29 +++++++++++++++++++ .../PhabricatorStandardCustomFieldDate.php | 4 +++ ...PhabricatorStandardCustomFieldRemarkup.php | 4 +++ .../field/PhabricatorEpochExportField.php | 12 ++++++++ .../format/PhabricatorExcelExportFormat.php | 15 ++++++++-- 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/applications/config/check/PhabricatorZipSetupCheck.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 095e7231fb..d050fd0ac5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5023,6 +5023,7 @@ phutil_register_library_map(array( 'PhabricatorXHProfSampleQuery' => 'applications/xhprof/query/PhabricatorXHProfSampleQuery.php', 'PhabricatorXHProfSampleSearchEngine' => 'applications/xhprof/query/PhabricatorXHProfSampleSearchEngine.php', 'PhabricatorYoutubeRemarkupRule' => 'infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php', + 'PhabricatorZipSetupCheck' => 'applications/config/check/PhabricatorZipSetupCheck.php', 'Phame404Response' => 'applications/phame/site/Phame404Response.php', 'PhameBlog' => 'applications/phame/storage/PhameBlog.php', 'PhameBlog404Controller' => 'applications/phame/controller/blog/PhameBlog404Controller.php', @@ -11449,6 +11450,7 @@ phutil_register_library_map(array( 'PhabricatorXHProfSampleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorXHProfSampleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorYoutubeRemarkupRule' => 'PhutilRemarkupRule', + 'PhabricatorZipSetupCheck' => 'PhabricatorSetupCheck', 'Phame404Response' => 'AphrontHTMLResponse', 'PhameBlog' => array( 'PhameDAO', diff --git a/src/applications/config/check/PhabricatorZipSetupCheck.php b/src/applications/config/check/PhabricatorZipSetupCheck.php new file mode 100644 index 0000000000..e440cd4c52 --- /dev/null +++ b/src/applications/config/check/PhabricatorZipSetupCheck.php @@ -0,0 +1,29 @@ +newIssue('extension.zip') + ->setName(pht('Missing "zip" Extension')) + ->setMessage($message) + ->addPHPExtension('zip'); + } + } +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php index 981d45b9b0..994bb99403 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php @@ -242,4 +242,8 @@ final class PhabricatorStandardCustomFieldDate return new ConduitEpochParameterType(); } + protected function newExportFieldType() { + return new PhabricatorEpochExportField(); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php index 7709233454..b0b9a3ef8e 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php @@ -107,4 +107,8 @@ final class PhabricatorStandardCustomFieldRemarkup return new ConduitStringParameterType(); } + protected function newExportFieldType() { + return new PhabricatorStringExportField(); + } + } diff --git a/src/infrastructure/export/field/PhabricatorEpochExportField.php b/src/infrastructure/export/field/PhabricatorEpochExportField.php index 4dffde5aa8..a4d03f72da 100644 --- a/src/infrastructure/export/field/PhabricatorEpochExportField.php +++ b/src/infrastructure/export/field/PhabricatorEpochExportField.php @@ -6,6 +6,10 @@ final class PhabricatorEpochExportField private $zone; public function getTextValue($value) { + if ($value === null) { + return ''; + } + if (!isset($this->zone)) { $this->zone = new DateTimeZone('UTC'); } @@ -21,12 +25,20 @@ final class PhabricatorEpochExportField } public function getNaturalValue($value) { + if ($value === null) { + return $value; + } + return (int)$value; } public function getPHPExcelValue($value) { $epoch = $this->getNaturalValue($value); + if ($epoch === null) { + return null; + } + $seconds_per_day = phutil_units('1 day in seconds'); $offset = ($seconds_per_day * 25569); diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php index 606df393d0..e7135bd9db 100644 --- a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php @@ -14,12 +14,23 @@ final class PhabricatorExcelExportFormat } public function isExportFormatEnabled() { - // TODO: PHPExcel has a dependency on the PHP zip extension. We should test - // for that here, since it fatals if we don't have the ZipArchive class. + if (!extension_loaded('zip')) { + return false; + } + return @include_once 'PHPExcel.php'; } public function getInstallInstructions() { + if (!extension_loaded('zip')) { + return pht(<< Date: Wed, 17 Jul 2019 12:23:32 -0700 Subject: [PATCH 29/54] Make workboard real-time updates mostly work Summary: Depends on D20654. Ref T4900. When a task is edited, emit a "workboards" event for all boards it appears on (in a future change, this should also include all boards it //previously// appeared on, and all parents of both sets of boards -- but I'm just getting things working for now). When we receive a "workboards" event, check if the visible board should be updated. Aphlict has a complicated intra-window leader/follower election system which could let us process this update event exactly once no matter how many windows a user has open with the same workboard. I'm not trying to do any of this since it seems fairly rare. It makes sense for events like "you have new notifications" where we don't want to generate 100 Ajax calls if the user has 100 windows open, but very few users seem likely to have 100 copies of the same workboard open. Test Plan: - Ran `bin/aphlict debug`. - Opened workboard A in two windows, X and Y. - Edited and moved tasks in window X. - Saw "workboards" messages in the Aphlict log. - Saw window Y update in nearly-real-time (locally, this is fast enough that it feels instantaneous). Then: - Stopped the Aphlcit server. - Edited a task. - Started the Aphlict server. - Saw window Y update after a few moments (i.e., update in response to a reconnect). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20656 --- resources/celerity/map.php | 28 ++++++++--------- .../editor/ManiphestTransactionEditor.php | 29 ++++++++++++++++++ .../js/application/projects/WorkboardBoard.js | 30 +++++++++++++++++++ 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index febcddca82..90ef62ed1b 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,7 +412,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '19df903f', + 'rsrc/js/application/projects/WorkboardBoard.js' => '75727403', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '84f82dad', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -743,7 +743,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '19df903f', + 'javelin-workboard-board' => '75727403', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '84f82dad', 'javelin-workboard-column' => 'c3d24e63', @@ -1030,18 +1030,6 @@ return array( '17b71bbc' => array( 'phui-theme-css', ), - '19df903f' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '1b6acc2a' => array( 'javelin-magical-init', 'javelin-util', @@ -1561,6 +1549,18 @@ return array( 'javelin-uri', 'javelin-request', ), + 75727403 => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '78bc5d94' => array( 'javelin-behavior', 'javelin-uri', diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 6255903eff..235c73280b 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -859,4 +859,33 @@ final class ManiphestTransactionEditor return array_values($phid_list); } + + protected function didApplyTransactions($object, array $xactions) { + // TODO: This should include projects which the object was previously + // associated with but no longer is (so it can be removed from those + // boards) but currently does not. + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($object->getPHID())) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + + $edge_query->execute(); + + $project_phids = $edge_query->getDestinationPHIDs(); + + if ($project_phids) { + $data = array( + 'type' => 'workboards', + 'subscribers' => $project_phids, + ); + + PhabricatorNotificationClient::tryToPostMessage($data); + } + + return $xactions; + } + } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index c78fcd4eaf..2f904b9f68 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -136,6 +136,36 @@ JX.install('WorkboardBoard', { .setHandler(on_reload) .register(); + var board_phid = this.getPHID(); + + JX.Stratcom.listen('aphlict-server-message', null, function(e) { + var message = e.getData(); + + if (message.type != 'workboards') { + return; + } + + // Check if this update notification is about the currently visible + // board. If it is, update the board state. + + var found_board = false; + for (var ii = 0; ii < message.subscribers.length; ii++) { + var subscriber_phid = message.subscribers[ii]; + if (subscriber_phid === board_phid) { + found_board = true; + break; + } + } + + if (found_board) { + on_reload(); + } + }); + + JX.Stratcom.listen('aphlict-reconnect', null, function(e) { + on_reload(); + }); + for (var k in this._columns) { this._columns[k].redraw(); } From cb4add31164917bd4c56e97fd17d03cd9cb7a27b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 18 Jul 2019 10:19:03 -0700 Subject: [PATCH 30/54] In Ferret, allow documents with no title to match query terms by using LEFT JOIN on the "title" ranking field Summary: Fixes T13345. See D20650. Currently, `PhabricatorCursorPagedPolicyAwareQuery` does a JOIN against the "title" field so it can apply additional ranking/ordering conditions to the query. This means that documents with no title (which don't have this field) are always excluded from the result set. We'd prefer to include them, just not give them any bonus ranking/relevance boost. Use a LEFT JOIN so they get included. Test Plan: - Applied D20650 (diff 1), made it use raw `getTitle()` as the document title, indexed a paste with no title. - Searched for a term in the paste body. - Before change: no results. - After change: found result. {F6601159} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13345 Differential Revision: https://secure.phabricator.com/D20660 --- .../PhabricatorCursorPagedPolicyAwareQuery.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 978de0fcf8..a8300c9da2 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1825,6 +1825,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $table_map['rank'] = array( 'alias' => 'ft_rank', 'key' => PhabricatorSearchDocumentFieldType::FIELD_TITLE, + + // See T13345. Not every document has a title, so we want to LEFT JOIN + // this table to avoid excluding documents with no title that match + // the query in other fields. + 'optional' => true, ); $this->ferretTables = $table_map; @@ -2103,10 +2108,17 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery foreach ($this->ferretTables as $table) { $alias = $table['alias']; + if (empty($table['optional'])) { + $join_type = qsprintf($conn, 'JOIN'); + } else { + $join_type = qsprintf($conn, 'LEFT JOIN'); + } + $joins[] = qsprintf( $conn, - 'JOIN %T %T ON ft_doc.id = %T.documentID + '%Q %T %T ON ft_doc.id = %T.documentID AND %T.fieldKey = %s', + $join_type, $field_table, $alias, $alias, From f55aac49f4d7aab895fee1736b92eb6ac68f1431 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 18 Jul 2019 10:31:52 -0700 Subject: [PATCH 31/54] Rename "pastebin" database to "paste" Summary: See D20650. Long ago, this got added as "pastebin", but that's the name of another product/company, not a generic term for paste storage. Rename the database to `phabricator_paste`. (An alternate version of this patch would rename `phabricator_search` to `phabricator_bing`, `phabricator_countdown` to `phabricator_spacex`, `phabricator_pholio` to `phabricator_adobe_photoshop`, etc.) Test Plan: - Grepped for `pastebin`, now only found references in old patches. - Applied patches. - Browsed around Paste in the UI without encountering issues. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D20661 --- resources/sql/autopatches/20190718.paste.01.edge.sql | 2 ++ resources/sql/autopatches/20190718.paste.02.edgedata.sql | 2 ++ resources/sql/autopatches/20190718.paste.03.paste.sql | 2 ++ resources/sql/autopatches/20190718.paste.04.xaction.sql | 2 ++ resources/sql/autopatches/20190718.paste.05.comment.sql | 2 ++ src/applications/paste/storage/PhabricatorPasteDAO.php | 2 +- .../paste/storage/PhabricatorPasteTransaction.php | 2 +- .../storage/patch/PhabricatorBuiltinPatchList.php | 5 ++++- 8 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 resources/sql/autopatches/20190718.paste.01.edge.sql create mode 100644 resources/sql/autopatches/20190718.paste.02.edgedata.sql create mode 100644 resources/sql/autopatches/20190718.paste.03.paste.sql create mode 100644 resources/sql/autopatches/20190718.paste.04.xaction.sql create mode 100644 resources/sql/autopatches/20190718.paste.05.comment.sql diff --git a/resources/sql/autopatches/20190718.paste.01.edge.sql b/resources/sql/autopatches/20190718.paste.01.edge.sql new file mode 100644 index 0000000000..ba138a3b92 --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.01.edge.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.edge + TO {$NAMESPACE}_paste.edge; diff --git a/resources/sql/autopatches/20190718.paste.02.edgedata.sql b/resources/sql/autopatches/20190718.paste.02.edgedata.sql new file mode 100644 index 0000000000..18b0c3ff4e --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.02.edgedata.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.edgedata + TO {$NAMESPACE}_paste.edgedata; diff --git a/resources/sql/autopatches/20190718.paste.03.paste.sql b/resources/sql/autopatches/20190718.paste.03.paste.sql new file mode 100644 index 0000000000..cc8d100773 --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.03.paste.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.pastebin_paste + TO {$NAMESPACE}_paste.paste; diff --git a/resources/sql/autopatches/20190718.paste.04.xaction.sql b/resources/sql/autopatches/20190718.paste.04.xaction.sql new file mode 100644 index 0000000000..5ebfcdfeaf --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.04.xaction.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.pastebin_pastetransaction + TO {$NAMESPACE}_paste.paste_transaction; diff --git a/resources/sql/autopatches/20190718.paste.05.comment.sql b/resources/sql/autopatches/20190718.paste.05.comment.sql new file mode 100644 index 0000000000..0221d0f668 --- /dev/null +++ b/resources/sql/autopatches/20190718.paste.05.comment.sql @@ -0,0 +1,2 @@ +RENAME TABLE {$NAMESPACE}_pastebin.pastebin_pastetransaction_comment + TO {$NAMESPACE}_paste.paste_transaction_comment; diff --git a/src/applications/paste/storage/PhabricatorPasteDAO.php b/src/applications/paste/storage/PhabricatorPasteDAO.php index dd61ff7920..0decb81055 100644 --- a/src/applications/paste/storage/PhabricatorPasteDAO.php +++ b/src/applications/paste/storage/PhabricatorPasteDAO.php @@ -3,7 +3,7 @@ abstract class PhabricatorPasteDAO extends PhabricatorLiskDAO { public function getApplicationName() { - return 'pastebin'; + return 'paste'; } } diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php index 1cd77a7048..18bf984259 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransaction.php +++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php @@ -8,7 +8,7 @@ final class PhabricatorPasteTransaction const MAILTAG_COMMENT = 'paste-comment'; public function getApplicationName() { - return 'pastebin'; + return 'paste'; } public function getApplicationTransactionType() { diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 2d5245459b..8adcfa64df 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -67,7 +67,9 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'db.metamta' => array(), 'db.oauth_server' => array(), 'db.owners' => array(), - 'db.pastebin' => array(), + 'db.pastebin' => array( + 'dead' => true, + ), 'db.phame' => array(), 'db.phriction' => array(), 'db.project' => array(), @@ -113,6 +115,7 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'db.badges' => array(), 'db.packages' => array(), 'db.application' => array(), + 'db.paste' => array(), '0000.legacy.sql' => array( 'legacy' => 0, ), From 5dd489500155ae3b423ba051531eade4d7d4dd26 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 07:03:55 -0700 Subject: [PATCH 32/54] Move "Password Reset" email to "PeopleMailEngine" Summary: Ref T13343. This makes "Password Reset" email a little more consistent with other modern types of email. My expectation is that this patch has no functional changes, just organizes code a little more consistently. The new `setRecipientAddress()` mechanism deals with the case where the user types a secondary (but still verified) address. Test Plan: - Sent a normal "login with email" email. - Sent a "login with email to set password" email by trying to set a password on an account with no password yet. - Tried to email reset a bot account (no dice: they can't do web logins so this operation isn't valid). - Tested existing "PeopleMailEngine" subclasses: - Created a new user and sent a "welcome" email. - Renamed a user and sent a "username changed" email. - Reviewed all generated mail with `bin/mail list-outbound`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20662 --- src/__phutil_library_map__.php | 2 + .../PhabricatorEmailLoginController.php | 78 +++---------- .../PhabricatorPeopleEmailLoginMailEngine.php | 107 ++++++++++++++++++ .../mail/PhabricatorPeopleMailEngine.php | 26 ++++- .../PhabricatorPeopleUsernameMailEngine.php | 2 - .../PhabricatorPeopleWelcomeMailEngine.php | 1 - 6 files changed, 151 insertions(+), 65 deletions(-) create mode 100644 src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d050fd0ac5..dc94bfd726 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4001,6 +4001,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleDeleteController' => 'applications/people/controller/PhabricatorPeopleDeleteController.php', 'PhabricatorPeopleDetailsProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php', 'PhabricatorPeopleDisableController' => 'applications/people/controller/PhabricatorPeopleDisableController.php', + 'PhabricatorPeopleEmailLoginMailEngine' => 'applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php', 'PhabricatorPeopleEmpowerController' => 'applications/people/controller/PhabricatorPeopleEmpowerController.php', 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', 'PhabricatorPeopleIconSet' => 'applications/people/icon/PhabricatorPeopleIconSet.php', @@ -10231,6 +10232,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleDeleteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorPeopleDisableController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleEmailLoginMailEngine' => 'PhabricatorPeopleMailEngine', 'PhabricatorPeopleEmpowerController' => 'PhabricatorPeopleController', 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleIconSet' => 'PhabricatorIconSet', diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 76b288f059..234829634b 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -94,29 +94,34 @@ final class PhabricatorEmailLoginController } if (!$errors) { - $body = $this->newAccountLoginMailBody( - $target_user, - $is_logged_in); + $target_address = new PhutilEmailAddress($target_email->getAddress()); + + $mail_engine = id(new PhabricatorPeopleEmailLoginMailEngine()) + ->setSender($viewer) + ->setRecipient($target_user) + ->setRecipientAddress($target_address); + + try { + $mail_engine->validateMail(); + } catch (PhabricatorPeopleMailEngineException $ex) { + return $this->newDialog() + ->setTitle($ex->getTitle()) + ->appendParagraph($ex->getBody()) + ->addCancelButton('/auth/start/', pht('Done')); + } + + $mail_engine->sendMail(); if ($is_logged_in) { - $subject = pht('[Phabricator] Account Password Link'); $instructions = pht( 'An email has been sent containing a link you can use to set '. 'a password for your account.'); } else { - $subject = pht('[Phabricator] Account Login Link'); $instructions = pht( 'An email has been sent containing a link you can use to log '. 'in to your account.'); } - $mail = id(new PhabricatorMetaMTAMail()) - ->setSubject($subject) - ->setForceDelivery(true) - ->addRawTos(array($target_email->getAddress())) - ->setBody($body) - ->saveAndSend(); - return $this->newDialog() ->setTitle(pht('Check Your Email')) ->setShortTitle(pht('Email Sent')) @@ -182,55 +187,6 @@ final class PhabricatorEmailLoginController ->addSubmitButton(pht('Send Email')); } - private function newAccountLoginMailBody( - PhabricatorUser $user, - $is_logged_in) { - - $engine = new PhabricatorAuthSessionEngine(); - $uri = $engine->getOneTimeLoginURI( - $user, - null, - PhabricatorAuthSessionEngine::ONETIME_RESET); - - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $have_passwords = $this->isPasswordAuthEnabled(); - - if ($have_passwords) { - if ($is_logged_in) { - $body = pht( - 'You can use this link to set a password on your account:'. - "\n\n %s\n", - $uri); - } else if ($is_serious) { - $body = pht( - "You can use this link to reset your Phabricator password:". - "\n\n %s\n", - $uri); - } else { - $body = pht( - "Condolences on forgetting your password. You can use this ". - "link to reset it:\n\n". - " %s\n\n". - "After you set a new password, consider writing it down on a ". - "sticky note and attaching it to your monitor so you don't ". - "forget again! Choosing a very short, easy-to-remember password ". - "like \"cat\" or \"1234\" might also help.\n\n". - "Best Wishes,\nPhabricator\n", - $uri); - - } - } else { - $body = pht( - "You can use this login link to regain access to your Phabricator ". - "account:". - "\n\n". - " %s\n", - $uri); - } - - return $body; - } - private function isPasswordAuthEnabled() { return (bool)PhabricatorPasswordAuthProvider::getPasswordProvider(); } diff --git a/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php b/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php new file mode 100644 index 0000000000..504d6c01cf --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php @@ -0,0 +1,107 @@ +getRecipient(); + + if ($recipient->getIsDisabled()) { + $this->throwValidationException( + pht('User is Disabled'), + pht( + 'You can not send an email login link to this email address '. + 'because the associated user account is disabled.')); + } + + if (!$recipient->canEstablishWebSessions()) { + $this->throwValidationException( + pht('Not a Normal User'), + pht( + 'You can not send an email login link to this email address '. + 'because the associated user account is not a normal user account '. + 'and can not log in to the web interface.')); + } + } + + protected function newMail() { + $is_set_password = $this->isSetPasswordWorkflow(); + + if ($is_set_password) { + $subject = pht('[Phabricator] Account Password Link'); + } else { + $subject = pht('[Phabricator] Account Login Link'); + } + + $recipient = $this->getRecipient(); + $engine = new PhabricatorAuthSessionEngine(); + $login_uri = $engine->getOneTimeLoginURI( + $recipient, + null, + PhabricatorAuthSessionEngine::ONETIME_RESET); + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + $have_passwords = $this->isPasswordAuthEnabled(); + + if ($have_passwords) { + if ($is_set_password) { + $body = pht( + 'You can use this link to set a password on your account:'. + "\n\n %s\n", + $login_uri); + } else if ($is_serious) { + $body = pht( + "You can use this link to reset your Phabricator password:". + "\n\n %s\n", + $login_uri); + } else { + $body = pht( + "Condolences on forgetting your password. You can use this ". + "link to reset it:\n\n". + " %s\n\n". + "After you set a new password, consider writing it down on a ". + "sticky note and attaching it to your monitor so you don't ". + "forget again! Choosing a very short, easy-to-remember password ". + "like \"cat\" or \"1234\" might also help.\n\n". + "Best Wishes,\nPhabricator\n", + $login_uri); + + } + } else { + $body = pht( + "You can use this login link to regain access to your Phabricator ". + "account:". + "\n\n". + " %s\n", + $login_uri); + } + + return id(new PhabricatorMetaMTAMail()) + ->setSubject($subject) + ->setBody($body); + } + + private function isPasswordAuthEnabled() { + return (bool)PhabricatorPasswordAuthProvider::getPasswordProvider(); + } + + private function isSetPasswordWorkflow() { + $sender = $this->getSender(); + $recipient = $this->getRecipient(); + + // Users can hit the "login with an email link" workflow while trying to + // set a password on an account which does not yet have a password. We + // require they verify that they own the email address and send them + // through the email login flow. In this case, the messaging is slightly + // different. + + if ($sender->getPHID()) { + if ($sender->getPHID() === $recipient->getPHID()) { + return true; + } + } + + return false; + } + +} diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php index 281009341d..b2f022bcdf 100644 --- a/src/applications/people/mail/PhabricatorPeopleMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -5,6 +5,7 @@ abstract class PhabricatorPeopleMailEngine private $sender; private $recipient; + private $recipientAddress; final public function setSender(PhabricatorUser $sender) { $this->sender = $sender; @@ -30,6 +31,22 @@ abstract class PhabricatorPeopleMailEngine return $this->recipient; } + final public function setRecipientAddress(PhutilEmailAddress $address) { + $this->recipientAddress = $address; + return $this; + } + + final public function getRecipientAddress() { + if (!$this->recipientAddress) { + throw new PhutilInvalidStateException('recipientAddress'); + } + return $this->recipientAddress; + } + + final public function hasRecipientAddress() { + return ($this->recipientAddress !== null); + } + final public function canSendMail() { try { $this->validateMail(); @@ -43,6 +60,14 @@ abstract class PhabricatorPeopleMailEngine $this->validateMail(); $mail = $this->newMail(); + if ($this->hasRecipientAddress()) { + $recipient_address = $this->getRecipientAddress(); + $mail->addRawTos(array($recipient_address->getAddress())); + } else { + $recipient = $this->getRecipient(); + $mail->addTos(array($recipient->getPHID())); + } + $mail ->setForceDelivery(true) ->save(); @@ -53,7 +78,6 @@ abstract class PhabricatorPeopleMailEngine abstract public function validateMail(); abstract protected function newMail(); - final protected function throwValidationException($title, $body) { throw new PhabricatorPeopleMailEngineException($title, $body); } diff --git a/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php index c954b7c38e..e62a6a4859 100644 --- a/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php @@ -30,7 +30,6 @@ final class PhabricatorPeopleUsernameMailEngine protected function newMail() { $sender = $this->getSender(); - $recipient = $this->getRecipient(); $sender_username = $sender->getUsername(); $sender_realname = $sender->getRealName(); @@ -52,7 +51,6 @@ final class PhabricatorPeopleUsernameMailEngine $new_username)); return id(new PhabricatorMetaMTAMail()) - ->addTos(array($recipient->getPHID())) ->setSubject(pht('[Phabricator] Username Changed')) ->setBody($body); } diff --git a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php index ff7ee71272..ec99a5a484 100644 --- a/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php @@ -104,7 +104,6 @@ final class PhabricatorPeopleWelcomeMailEngine $message = implode("\n\n", $message); return id(new PhabricatorMetaMTAMail()) - ->addTos(array($recipient->getPHID())) ->setSubject(pht('[Phabricator] Welcome to Phabricator')) ->setBody($message); } From a0c9f9f90c819998fe88e687342415c97f322d48 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 07:33:54 -0700 Subject: [PATCH 33/54] Allow installs to customize mail body guidance in the "Email Login" and "Set Password" emails Summary: Depends on D20662. Ref T13343. Installs may reasonably want to change the guidance users receive in "Email Login"/"Forgot Password" email. (In an upcoming change I plan to supply a piece of default guidance, but Auth Messages need a few tweaks for this.) There's probably little reason to provide guidance on the "Set Password" flow, but any guidance one might issue on the "Email Login" flow probably doesn't make sense on the "Set Password" flow, so I've included it mostly to make it clear that this is a different flow from a user perspective. Test Plan: - Set custom "Email Login" and "Set Password" messages. - Generated "Email Login" mail by using the "Login via email" link on the login screen. - Generated "Set Password" email by trying to set a password on an account with no password yet. - Saw my custom messages in the resulting mail bodies. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20663 --- src/__phutil_library_map__.php | 4 +++ .../PhabricatorAuthEmailLoginMessageType.php | 18 +++++++++++++ ...ricatorAuthEmailSetPasswordMessageType.php | 18 +++++++++++++ .../message/PhabricatorAuthMessageType.php | 1 + .../PhabricatorAuthWelcomeMailMessageType.php | 2 +- .../auth/storage/PhabricatorAuthMessage.php | 2 +- .../PhabricatorPeopleEmailLoginMailEngine.php | 25 ++++++++++++++++--- 7 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php create mode 100644 src/applications/auth/message/PhabricatorAuthEmailSetPasswordMessageType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index dc94bfd726..93dc753ded 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2265,6 +2265,8 @@ phutil_register_library_map(array( 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php', 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', + 'PhabricatorAuthEmailLoginMessageType' => 'applications/auth/message/PhabricatorAuthEmailLoginMessageType.php', + 'PhabricatorAuthEmailSetPasswordMessageType' => 'applications/auth/message/PhabricatorAuthEmailSetPasswordMessageType.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', 'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php', @@ -8220,6 +8222,8 @@ phutil_register_library_map(array( 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', + 'PhabricatorAuthEmailLoginMessageType' => 'PhabricatorAuthMessageType', + 'PhabricatorAuthEmailSetPasswordMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => array( 'PhabricatorAuthDAO', diff --git a/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php b/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php new file mode 100644 index 0000000000..a866dfa9e2 --- /dev/null +++ b/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php @@ -0,0 +1,18 @@ +getID()); + return urisprintf('/auth/message/%s/', $this->getID()); } public function attachMessageType(PhabricatorAuthMessageType $type) { diff --git a/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php b/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php index 504d6c01cf..325309c71b 100644 --- a/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleEmailLoginMailEngine.php @@ -43,19 +43,34 @@ final class PhabricatorPeopleEmailLoginMailEngine $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $have_passwords = $this->isPasswordAuthEnabled(); + $body = array(); + + if ($is_set_password) { + $message_key = PhabricatorAuthEmailSetPasswordMessageType::MESSAGEKEY; + } else { + $message_key = PhabricatorAuthEmailLoginMessageType::MESSAGEKEY; + } + + $message_body = PhabricatorAuthMessage::loadMessageText( + $recipient, + $message_key); + if (strlen($message_body)) { + $body[] = $this->newRemarkupText($message_body); + } + if ($have_passwords) { if ($is_set_password) { - $body = pht( + $body[] = pht( 'You can use this link to set a password on your account:'. "\n\n %s\n", $login_uri); } else if ($is_serious) { - $body = pht( + $body[] = pht( "You can use this link to reset your Phabricator password:". "\n\n %s\n", $login_uri); } else { - $body = pht( + $body[] = pht( "Condolences on forgetting your password. You can use this ". "link to reset it:\n\n". " %s\n\n". @@ -68,7 +83,7 @@ final class PhabricatorPeopleEmailLoginMailEngine } } else { - $body = pht( + $body[] = pht( "You can use this login link to regain access to your Phabricator ". "account:". "\n\n". @@ -76,6 +91,8 @@ final class PhabricatorPeopleEmailLoginMailEngine $login_uri); } + $body = implode("\n\n", $body); + return id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->setBody($body); From 38d30af362e60452c1f2a89e65455cbd0a4713f6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 08:57:50 -0700 Subject: [PATCH 34/54] Give "Auth Messages" a view/detail state before users customize them Summary: Depends on D20663. Ref T13343. Currently, if an Auth message hasn't been customized yet, clicking the message type takes you straight to an edit screen to create a message. If an auth message has already been customized, you go to a detail screen instead. Since there's no detail screen on the "create for the first time" flow, we don't have anywhere to put a more detailed description or a preview of a default value. Add a view screen that works if a message is "empty" so we can add this stuff. (The only reason we don't already have this is that it took a little work to build; this also generally improves the consistency and predictability of this interface.) Test Plan: {F6607665} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20664 --- .../PhabricatorAuthApplication.php | 2 +- .../PhabricatorAuthMessageListController.php | 5 +- .../PhabricatorAuthMessageViewController.php | 87 +++++++++++++++---- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 3446ad597d..6d4a70b735 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -108,7 +108,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthMessageListController', $this->getEditRoutePattern('edit/') => 'PhabricatorAuthMessageEditController', - '(?P[1-9]\d*)/' => + '(?P[^/]+)/' => 'PhabricatorAuthMessageViewController', ), diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php index a3c518ab36..7981a03f16 100644 --- a/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageListController.php @@ -19,11 +19,14 @@ final class PhabricatorAuthMessageListController $list = new PHUIObjectItemListView(); foreach ($types as $type) { $message = idx($messages, $type->getMessageTypeKey()); + if ($message) { $href = $message->getURI(); $name = $message->getMessageTypeDisplayName(); } else { - $href = '/auth/message/edit/?messageKey='.$type->getMessageTypeKey(); + $href = urisprintf( + '/auth/message/%s/', + $type->getMessageTypeKey()); $name = $type->getDisplayName(); } diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php index db7e7e65e0..fab5dcafb0 100644 --- a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php @@ -9,26 +9,61 @@ final class PhabricatorAuthMessageViewController $this->requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); - $message = id(new PhabricatorAuthMessageQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) - ->executeOne(); - if (!$message) { - return new Aphront404Response(); + // The "id" in the URI may either be an actual storage record ID (if a + // message has already been created) or a message type key (for a message + // type which does not have a record yet). + + // This flow allows messages which have not been set yet to have a detail + // page (so users can get detailed information about the message and see + // any default value). + + $id = $request->getURIData('id'); + if (ctype_digit($id)) { + $message = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$message) { + return new Aphront404Response(); + } + } else { + $types = PhabricatorAuthMessageType::getAllMessageTypes(); + if (!isset($types[$id])) { + return new Aphront404Response(); + } + + // If this message type already has a storage record, redirect to the + // canonical page for the record. + $message = id(new PhabricatorAuthMessageQuery()) + ->setViewer($viewer) + ->withMessageKeys(array($id)) + ->executeOne(); + if ($message) { + $message_uri = $message->getURI(); + return id(new AphrontRedirectResponse())->setURI($message_uri); + } + + // Otherwise, create an empty placeholder message object with the + // appropriate message type. + $message = PhabricatorAuthMessage::initializeNewMessage($types[$id]); } $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb($message->getObjectName()) + ->addTextCrumb($message->getMessageType()->getDisplayName()) ->setBorder(true); $header = $this->buildHeaderView($message); $properties = $this->buildPropertiesView($message); $curtain = $this->buildCurtain($message); - $timeline = $this->buildTransactionTimeline( - $message, - new PhabricatorAuthMessageTransactionQuery()); - $timeline->setShouldTerminate(true); + if ($message->getID()) { + $timeline = $this->buildTransactionTimeline( + $message, + new PhabricatorAuthMessageTransactionQuery()); + $timeline->setShouldTerminate(true); + } else { + $timeline = null; + } $view = id(new PHUITwoColumnView()) ->setHeader($header) @@ -69,12 +104,14 @@ final class PhabricatorAuthMessageViewController pht('Description'), $message->getMessageType()->getShortDescription()); - $view->addSectionHeader( - pht('Message Preview'), - PHUIPropertyListView::ICON_SUMMARY); + if (strlen($message->getMessageText())) { + $view->addSectionHeader( + pht('Message Preview'), + PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent( - new PHUIRemarkupView($viewer, $message->getMessageText())); + $view->addTextContent( + new PHUIRemarkupView($viewer, $message->getMessageText())); + } return $view; } @@ -88,13 +125,27 @@ final class PhabricatorAuthMessageViewController $message, PhabricatorPolicyCapability::CAN_EDIT); + if ($id) { + $edit_uri = urisprintf('message/edit/%s/', $id); + $edit_name = pht('Edit Message'); + } else { + $edit_uri = urisprintf('message/edit/'); + $params = array( + 'messageKey' => $message->getMessageKey(), + ); + $edit_uri = new PhutilURI($edit_uri, $params); + + $edit_name = pht('Customize Message'); + } + $edit_uri = $this->getApplicationURI($edit_uri); + $curtain = $this->newCurtainView($message); $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Edit Message')) + ->setName($edit_name) ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("message/edit/{$id}/")) + ->setHref($edit_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); From ced416cc735a70a1ae64c40afa54cf98c6e9eed4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 09:22:52 -0700 Subject: [PATCH 35/54] Allow Auth messages to have detailed descriptions and default values, then give "Email Login" both Summary: Depends on D20664. Ref T13343. There's a reasonable value for the default "Email Login" auth message (generic "you reset your password" text) that installs may reasonably want to replace. Add support for a default value. Also, since it isn't completely obvious where this message shows up, add support for an extended description and explain what's going on in more detail. Test Plan: - Viewed message detail page, saw more detailed information. - Sent mail (got default), overrode message and sent mail (got custom message), deleted message (got default again). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20665 --- .../PhabricatorAuthMessageViewController.php | 27 ++++++++++++++----- .../PhabricatorAuthEmailLoginMessageType.php | 23 ++++++++++++++++ .../message/PhabricatorAuthMessageType.php | 8 ++++++ .../auth/storage/PhabricatorAuthMessage.php | 12 ++++++--- .../mail/PhabricatorPeopleMailEngine.php | 5 +++- 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php index fab5dcafb0..5665744463 100644 --- a/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php +++ b/src/applications/auth/controller/message/PhabricatorAuthMessageViewController.php @@ -97,20 +97,35 @@ final class PhabricatorAuthMessageViewController private function buildPropertiesView(PhabricatorAuthMessage $message) { $viewer = $this->getViewer(); + $message_type = $message->getMessageType(); + $view = id(new PHUIPropertyListView()) ->setViewer($viewer); - $view->addProperty( - pht('Description'), - $message->getMessageType()->getShortDescription()); + $full_description = $message_type->getFullDescription(); + if (strlen($full_description)) { + $view->addTextContent(new PHUIRemarkupView($viewer, $full_description)); + } else { + $short_description = $message_type->getShortDescription(); + $view->addProperty(pht('Description'), $short_description); + } - if (strlen($message->getMessageText())) { + $message_text = $message->getMessageText(); + if (strlen($message_text)) { $view->addSectionHeader( pht('Message Preview'), PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent( - new PHUIRemarkupView($viewer, $message->getMessageText())); + $view->addTextContent(new PHUIRemarkupView($viewer, $message_text)); + } + + $default_text = $message_type->getDefaultMessageText(); + if (strlen($default_text)) { + $view->addSectionHeader( + pht('Default Message'), + PHUIPropertyListView::ICON_SUMMARY); + + $view->addTextContent(new PHUIRemarkupView($viewer, $default_text)); } return $view; diff --git a/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php b/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php index a866dfa9e2..0bb55a7461 100644 --- a/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php +++ b/src/applications/auth/message/PhabricatorAuthEmailLoginMessageType.php @@ -15,4 +15,27 @@ final class PhabricatorAuthEmailLoginMessageType 'to access their account.'); } + public function getFullDescription() { + return pht( + 'Guidance included in the mail message body when users request an '. + 'email link to access their account.'. + "\n\n". + 'For installs with password authentication enabled, users access this '. + 'workflow by using the "Forgot your password?" link on the login '. + 'screen.'. + "\n\n". + 'For installs without password authentication enabled, users access '. + 'this workflow by using the "Send a login link to your email address." '. + 'link on the login screen. This workflow allows users to recover '. + 'access to their account if there is an issue with an external '. + 'login service.'); + } + + public function getDefaultMessageText() { + return pht( + 'You (or someone pretending to be you) recently requested an account '. + 'recovery link be sent to this email address. If you did not make '. + 'this request, you can ignore this message.'); + } + } diff --git a/src/applications/auth/message/PhabricatorAuthMessageType.php b/src/applications/auth/message/PhabricatorAuthMessageType.php index f883cb4146..9474eee1f3 100644 --- a/src/applications/auth/message/PhabricatorAuthMessageType.php +++ b/src/applications/auth/message/PhabricatorAuthMessageType.php @@ -30,4 +30,12 @@ abstract class PhabricatorAuthMessageType abstract public function getDisplayName(); abstract public function getShortDescription(); + public function getFullDescription() { + return null; + } + + public function getDefaultMessageText() { + return null; + } + } diff --git a/src/applications/auth/storage/PhabricatorAuthMessage.php b/src/applications/auth/storage/PhabricatorAuthMessage.php index f12550e440..9969d7aded 100644 --- a/src/applications/auth/storage/PhabricatorAuthMessage.php +++ b/src/applications/auth/storage/PhabricatorAuthMessage.php @@ -75,12 +75,16 @@ final class PhabricatorAuthMessage $message_key) { $message = self::loadMessage($viewer, $message_key); - - if (!$message) { - return null; + if ($message) { + $message_text = $message->getMessageText(); + if (strlen($message_text)) { + return $message_text; + } } - return $message->getMessageText(); + $message_type = PhabricatorAuthMessageType::newFromKey($message_key); + + return $message_type->getDefaultMessageText(); } diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php index b2f022bcdf..c1379dda9e 100644 --- a/src/applications/people/mail/PhabricatorPeopleMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -90,7 +90,10 @@ abstract class PhabricatorPeopleMailEngine ->setConfig('uri.base', PhabricatorEnv::getProductionURI('/')) ->setMode(PhutilRemarkupEngine::MODE_TEXT); - return $engine->markupText($text); + $rendered_text = $engine->markupText($text); + $rendered_text = rtrim($rendered_text, "\n"); + + return $rendered_text; } } From 80294e7a4ad1c8a07cf1b305f1f0c3056ba1052b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 09:38:20 -0700 Subject: [PATCH 36/54] Add a rate limit to generating new account recovery links for a given account Summary: Depends on D20665. Ref T13343. We support CAPTCHAs on the "Forgot password?" flow, but not everyone configures them (or necessarily should, since ReCAPTCHA is a huge external dependency run by Google that requires you allow Google to execute JS on your domain) and the rate at which any reasonable user needs to take this action is very low. Put a limit on the rate at which account recovery links may be generated for a particular account, so the worst case is a trickle of annoyance rather than a flood of nonsense. Test Plan: {F6607794} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20666 --- src/__phutil_library_map__.php | 2 ++ .../PhabricatorAuthEmailLoginAction.php | 21 +++++++++++++++++++ .../PhabricatorPeopleEmailLoginMailEngine.php | 6 ++++++ 3 files changed, 29 insertions(+) create mode 100644 src/applications/auth/action/PhabricatorAuthEmailLoginAction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 93dc753ded..3120eb01b6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2265,6 +2265,7 @@ phutil_register_library_map(array( 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', 'PhabricatorAuthDowngradeSessionController' => 'applications/auth/controller/PhabricatorAuthDowngradeSessionController.php', 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', + 'PhabricatorAuthEmailLoginAction' => 'applications/auth/action/PhabricatorAuthEmailLoginAction.php', 'PhabricatorAuthEmailLoginMessageType' => 'applications/auth/message/PhabricatorAuthEmailLoginMessageType.php', 'PhabricatorAuthEmailSetPasswordMessageType' => 'applications/auth/message/PhabricatorAuthEmailSetPasswordMessageType.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', @@ -8222,6 +8223,7 @@ phutil_register_library_map(array( 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', + 'PhabricatorAuthEmailLoginAction' => 'PhabricatorSystemAction', 'PhabricatorAuthEmailLoginMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthEmailSetPasswordMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthFactor' => 'Phobject', diff --git a/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php b/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php new file mode 100644 index 0000000000..c7729047b4 --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php @@ -0,0 +1,21 @@ +getRecipient(); + + PhabricatorSystemActionEngine::willTakeAction( + array($recipient->getPHID()), + new PhabricatorAuthEmailLoginAction(), + 1); + $engine = new PhabricatorAuthSessionEngine(); $login_uri = $engine->getOneTimeLoginURI( $recipient, From e090b32c7528498fa5e742efeb797f18b3acfdc2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 09:56:48 -0700 Subject: [PATCH 37/54] Add a rate limit to requesting account recovery links from a given remote address Summary: Depends on D20666. Ref T13343. In D20666, I limited the rate at which a given user account can be sent account recovery links. Here, add a companion limit to the rate at which a given remote address may request recovery of any account. This limit is a little more forgiving since reasonable users may plausibly try multiple variations of several email addresses, make typos, etc. The goal is just to hinder attackers from fishing for every address under the sun on installs with no CAPTCHA configured and no broad-spectrum VPN-style access controls. Test Plan: {F6607846} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20667 --- src/__phutil_library_map__.php | 2 ++ .../PhabricatorAuthTryEmailLoginAction.php | 22 +++++++++++++++++++ .../PhabricatorEmailLoginController.php | 8 +++++++ .../engine/PhabricatorSystemActionEngine.php | 4 ++++ 4 files changed, 36 insertions(+) create mode 100644 src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3120eb01b6..6ccaf1ec97 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2429,6 +2429,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php', 'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php', 'PhabricatorAuthTestSMSAction' => 'applications/auth/action/PhabricatorAuthTestSMSAction.php', + 'PhabricatorAuthTryEmailLoginAction' => 'applications/auth/action/PhabricatorAuthTryEmailLoginAction.php', 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', @@ -8424,6 +8425,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule', 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthTryEmailLoginAction' => 'PhabricatorSystemAction', 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', diff --git a/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php b/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php new file mode 100644 index 0000000000..001358e3f6 --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php @@ -0,0 +1,22 @@ +loadOneWhere( 'address = %s', $v_email); diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php index 6b8352a29e..c097fa04a4 100644 --- a/src/applications/system/engine/PhabricatorSystemActionEngine.php +++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php @@ -198,4 +198,8 @@ final class PhabricatorSystemActionEngine extends Phobject { return $conn_w->getAffectedRows(); } + public static function newActorFromRequest(AphrontRequest $request) { + return $request->getRemoteAddress(); + } + } From a75766c0e501c75b9d526942a504fc875da6c990 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 10:11:55 -0700 Subject: [PATCH 38/54] Replace old rate limiting in password login flow with "SystemAction" rate limiting Summary: Depends on D20667. Ref T13343. Password auth currently uses an older rate limiting mechanism, upgrade it to the modern "SystemAction" mechanism. This mostly just improves consistency, although there are some tangential/theoretical benefits: - it's not obvious that making the user log GC very quickly could disable rate limiting; - if we let you configure action limits in the future, which we might, this would become configurable for free. Test Plan: - With CAPTCHAs off, made a bunch of invalid login attempts. Got rate limited. - With CAPTCHAs on, made a bunch of invalid login attempts. Got downgraded to CAPTCHAs after a few. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20668 --- src/__phutil_library_map__.php | 4 ++ .../PhabricatorAuthTryPasswordAction.php | 22 ++++++++++ ...torAuthTryPasswordWithoutCAPTCHAAction.php | 16 ++++++++ .../PhabricatorPasswordAuthProvider.php | 41 +++++-------------- 4 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 src/applications/auth/action/PhabricatorAuthTryPasswordAction.php create mode 100644 src/applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6ccaf1ec97..a82181acd7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2431,6 +2431,8 @@ phutil_register_library_map(array( 'PhabricatorAuthTestSMSAction' => 'applications/auth/action/PhabricatorAuthTestSMSAction.php', 'PhabricatorAuthTryEmailLoginAction' => 'applications/auth/action/PhabricatorAuthTryEmailLoginAction.php', 'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php', + 'PhabricatorAuthTryPasswordAction' => 'applications/auth/action/PhabricatorAuthTryPasswordAction.php', + 'PhabricatorAuthTryPasswordWithoutCAPTCHAAction' => 'applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php', 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', 'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php', 'PhabricatorAuthWaitForApprovalMessageType' => 'applications/auth/message/PhabricatorAuthWaitForApprovalMessageType.php', @@ -8427,6 +8429,8 @@ phutil_register_library_map(array( 'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction', 'PhabricatorAuthTryEmailLoginAction' => 'PhabricatorSystemAction', 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthTryPasswordAction' => 'PhabricatorSystemAction', + 'PhabricatorAuthTryPasswordWithoutCAPTCHAAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', 'PhabricatorAuthWaitForApprovalMessageType' => 'PhabricatorAuthMessageType', diff --git a/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php b/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php new file mode 100644 index 0000000000..440cbf301e --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php @@ -0,0 +1,22 @@ +getUser(); $content_source = PhabricatorContentSource::newFromRequest($request); - $captcha_limit = 5; - $hard_limit = 32; - $limit_window = phutil_units('15 minutes in seconds'); + $rate_actor = PhabricatorSystemActionEngine::newActorFromRequest($request); - $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( - PhabricatorUserLog::ACTION_LOGIN_FAILURE, - $limit_window); + PhabricatorSystemActionEngine::willTakeAction( + array($rate_actor), + new PhabricatorAuthTryPasswordAction(), + 1); // If the same remote address has submitted several failed login attempts // recently, require they provide a CAPTCHA response for new attempts. $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { - if (count($failed_attempts) > $captcha_limit) { + try { + PhabricatorSystemActionEngine::willTakeAction( + array($rate_actor), + new PhabricatorAuthTryPasswordWithoutCAPTCHAAction(), + 1); + } catch (PhabricatorSystemActionRateLimitException $ex) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } - // If the user has submitted quite a few failed login attempts recently, - // give them a hard limit. - if (count($failed_attempts) > $hard_limit) { - $guidance = array(); - - $guidance[] = pht( - 'Your remote address has failed too many login attempts recently. '. - 'Wait a few minutes before trying again.'); - - $guidance[] = pht( - 'If you are unable to log in to your account, you can '. - '[[ /login/email | send a reset link to your email address ]].'); - - $guidance = implode("\n\n", $guidance); - - $dialog = $controller->newDialog() - ->setTitle(pht('Too Many Login Attempts')) - ->appendChild(new PHUIRemarkupView($viewer, $guidance)) - ->addCancelButton('/auth/start/', pht('Wait Patiently')); - - return array(null, $dialog); - } - $response = null; $account = null; $log_user = null; From 2ee5e71029af82736fa4dd21d728a22f86981900 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 10:26:08 -0700 Subject: [PATCH 39/54] Simplify implementation of "SysetemAction->getSystemActionConstant()" Summary: Depends on D20668. Ref T13343. Just an easy cleanup/simplification while I'm here. Test Plan: `grep` for `getActionConstant()` Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20669 --- .../auth/action/PhabricatorAuthChangePasswordAction.php | 4 ---- .../auth/action/PhabricatorAuthEmailLoginAction.php | 4 ---- .../auth/action/PhabricatorAuthNewFactorAction.php | 4 ---- .../auth/action/PhabricatorAuthTestSMSAction.php | 4 ---- .../auth/action/PhabricatorAuthTryEmailLoginAction.php | 4 ---- .../auth/action/PhabricatorAuthTryFactorAction.php | 4 ---- .../auth/action/PhabricatorAuthTryPasswordAction.php | 4 ---- .../PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php | 4 ---- .../files/action/PhabricatorFilesOutboundRequestAction.php | 4 ---- .../metamta/action/PhabricatorMetaMTAErrorMailAction.php | 4 +--- .../phortune/action/PhortuneAddPaymentMethodAction.php | 4 ---- .../settings/action/PhabricatorSettingsAddEmailAction.php | 4 ---- src/applications/system/action/PhabricatorSystemAction.php | 5 ++++- 13 files changed, 5 insertions(+), 48 deletions(-) diff --git a/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php b/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php index 323c3e65b6..41aac1ec12 100644 --- a/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php +++ b/src/applications/auth/action/PhabricatorAuthChangePasswordAction.php @@ -5,10 +5,6 @@ final class PhabricatorAuthChangePasswordAction const TYPECONST = 'auth.password'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 20 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php b/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php index c7729047b4..97fe5b48e1 100644 --- a/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php +++ b/src/applications/auth/action/PhabricatorAuthEmailLoginAction.php @@ -4,10 +4,6 @@ final class PhabricatorAuthEmailLoginAction extends PhabricatorSystemAction { const TYPECONST = 'mail.login'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 3 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthNewFactorAction.php b/src/applications/auth/action/PhabricatorAuthNewFactorAction.php index c1244587f1..82b851d383 100644 --- a/src/applications/auth/action/PhabricatorAuthNewFactorAction.php +++ b/src/applications/auth/action/PhabricatorAuthNewFactorAction.php @@ -4,10 +4,6 @@ final class PhabricatorAuthNewFactorAction extends PhabricatorSystemAction { const TYPECONST = 'auth.factor.new'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 60 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthTestSMSAction.php b/src/applications/auth/action/PhabricatorAuthTestSMSAction.php index d0f4a6bb7e..a03f4d6877 100644 --- a/src/applications/auth/action/PhabricatorAuthTestSMSAction.php +++ b/src/applications/auth/action/PhabricatorAuthTestSMSAction.php @@ -4,10 +4,6 @@ final class PhabricatorAuthTestSMSAction extends PhabricatorSystemAction { const TYPECONST = 'auth.sms.test'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 60 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php b/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php index 001358e3f6..abb1a6c099 100644 --- a/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php +++ b/src/applications/auth/action/PhabricatorAuthTryEmailLoginAction.php @@ -5,10 +5,6 @@ final class PhabricatorAuthTryEmailLoginAction const TYPECONST = 'mail.try-login'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 20 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthTryFactorAction.php b/src/applications/auth/action/PhabricatorAuthTryFactorAction.php index 246298567b..d57b481213 100644 --- a/src/applications/auth/action/PhabricatorAuthTryFactorAction.php +++ b/src/applications/auth/action/PhabricatorAuthTryFactorAction.php @@ -4,10 +4,6 @@ final class PhabricatorAuthTryFactorAction extends PhabricatorSystemAction { const TYPECONST = 'auth.factor'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 10 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php b/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php index 440cbf301e..9ec6799c38 100644 --- a/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php +++ b/src/applications/auth/action/PhabricatorAuthTryPasswordAction.php @@ -5,10 +5,6 @@ final class PhabricatorAuthTryPasswordAction const TYPECONST = 'auth.password'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 100 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php b/src/applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php index ee6433a9ee..0e09820273 100644 --- a/src/applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php +++ b/src/applications/auth/action/PhabricatorAuthTryPasswordWithoutCAPTCHAAction.php @@ -5,10 +5,6 @@ final class PhabricatorAuthTryPasswordWithoutCAPTCHAAction const TYPECONST = 'auth.password-without-captcha'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 10 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php b/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php index acba2f8882..7a1d3d2d56 100644 --- a/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php +++ b/src/applications/files/action/PhabricatorFilesOutboundRequestAction.php @@ -5,10 +5,6 @@ final class PhabricatorFilesOutboundRequestAction const TYPECONST = 'files.outbound'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 60 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php b/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php index 0bc8b29172..cabb8c82b0 100644 --- a/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php +++ b/src/applications/metamta/action/PhabricatorMetaMTAErrorMailAction.php @@ -2,9 +2,7 @@ final class PhabricatorMetaMTAErrorMailAction extends PhabricatorSystemAction { - public function getActionConstant() { - return 'email.error'; - } + const TYPECONST = 'email.error'; public function getScoreThreshold() { return 6 / phutil_units('1 hour in seconds'); diff --git a/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php index 09a8cd2f5d..5f32e67cee 100644 --- a/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php +++ b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php @@ -5,10 +5,6 @@ final class PhortuneAddPaymentMethodAction const TYPECONST = 'phortune.payment-method.add'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 60 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php b/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php index 764db7f543..4038e37c9f 100644 --- a/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php +++ b/src/applications/settings/action/PhabricatorSettingsAddEmailAction.php @@ -4,10 +4,6 @@ final class PhabricatorSettingsAddEmailAction extends PhabricatorSystemAction { const TYPECONST = 'email.add'; - public function getActionConstant() { - return self::TYPECONST; - } - public function getScoreThreshold() { return 6 / phutil_units('1 hour in seconds'); } diff --git a/src/applications/system/action/PhabricatorSystemAction.php b/src/applications/system/action/PhabricatorSystemAction.php index 329824bacc..b712dfca8c 100644 --- a/src/applications/system/action/PhabricatorSystemAction.php +++ b/src/applications/system/action/PhabricatorSystemAction.php @@ -2,7 +2,10 @@ abstract class PhabricatorSystemAction extends Phobject { - abstract public function getActionConstant(); + final public function getActionConstant() { + return $this->getPhobjectClassConstant('TYPECONST', 32); + } + abstract public function getScoreThreshold(); public function shouldBlockActor($actor, $score) { From 4fd473e7eda6471d428d861615d2bd3b4cfca31c Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 10:57:13 -0700 Subject: [PATCH 40/54] Remove explicit administrative actions from the user activity log Summary: Depends on D20669. Ref T13343. Currently, the user activity log includes a number of explicit administrative actions which some administrator (not a normal user or a suspicious remote address) takes. In most/all cases, these changes are present in the user profile transaction log too, and that's //generally// a better place for them (for example, it doesn't get GC'd after a couple months). Some of these are so old that they have no writers (like DELETE and EDIT). I'd generally like to modernize this a bit so we can reference it in email (see T13343) and I'd like to modularize the event types as part of that -- partly, cleaning this up makes that modularization easier. There's maybe some hand-wavey argument that administrative vs non-administrative events could be related and might be useful to see in a single log, but I can't recall a time when that was actually true, and we could always build that kind of view later by just merging the two log sources, or by restoring double-writes for some subset of events. In practice, I've used this log mostly to look for obvious red flags when users report authentication difficulty (e.g., many unauthorized login attempts), and removing administrative actions from the log is only helpful in that use case. Test Plan: Grepped for all the affected constants, no more hits in the codebase. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20670 --- .../people/editor/PhabricatorUserEditor.php | 54 ------------------- .../people/storage/PhabricatorUserLog.php | 20 ------- .../PhabricatorUserApproveTransaction.php | 4 -- .../PhabricatorUserDisableTransaction.php | 7 --- .../PhabricatorUserEmpowerTransaction.php | 9 ---- .../PhabricatorUserTransactionType.php | 11 +--- .../PhabricatorUserUsernameTransaction.php | 5 -- 7 files changed, 1 insertion(+), 109 deletions(-) diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index c8068858da..9ab2c9d60b 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -74,13 +74,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { throw $ex; } - $log = PhabricatorUserLog::initializeNewLog( - $this->requireActor(), - $user->getPHID(), - PhabricatorUserLog::ACTION_CREATE); - $log->setNewValue($email->getAddress()); - $log->save(); - if ($is_reassign) { $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), @@ -100,35 +93,6 @@ final class PhabricatorUserEditor extends PhabricatorEditor { } - /** - * @task edit - */ - public function updateUser( - PhabricatorUser $user, - PhabricatorUserEmail $email = null) { - - if (!$user->getID()) { - throw new Exception(pht('User has not been created yet!')); - } - - $user->openTransaction(); - $user->save(); - if ($email) { - $email->save(); - } - - $log = PhabricatorUserLog::initializeNewLog( - $this->requireActor(), - $user->getPHID(), - PhabricatorUserLog::ACTION_EDIT); - $log->save(); - - $user->saveTransaction(); - - return $this; - } - - /* -( Editing Roles )------------------------------------------------------ */ /** @@ -151,18 +115,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_SYSTEM_AGENT); - $log->setOldValue($user->getIsSystemAgent()); - $log->setNewValue($system_agent); - $user->setIsSystemAgent((int)$system_agent); $user->save(); - $log->save(); - $user->endWriteLocking(); $user->saveTransaction(); @@ -189,18 +144,9 @@ final class PhabricatorUserEditor extends PhabricatorEditor { return $this; } - $log = PhabricatorUserLog::initializeNewLog( - $actor, - $user->getPHID(), - PhabricatorUserLog::ACTION_MAILING_LIST); - $log->setOldValue($user->getIsMailingList()); - $log->setNewValue($mailing_list); - $user->setIsMailingList((int)$mailing_list); $user->save(); - $log->save(); - $user->endWriteLocking(); $user->saveTransaction(); diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index 12cb4cb626..c7550ba8e5 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -11,16 +11,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO const ACTION_LOGIN_LEGALPAD = 'login-legalpad'; const ACTION_RESET_PASSWORD = 'reset-pass'; - const ACTION_CREATE = 'create'; - const ACTION_EDIT = 'edit'; - - const ACTION_ADMIN = 'admin'; - const ACTION_SYSTEM_AGENT = 'system-agent'; - const ACTION_MAILING_LIST = 'mailing-list'; - const ACTION_DISABLE = 'disable'; - const ACTION_APPROVE = 'approve'; - const ACTION_DELETE = 'delete'; - const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert'; const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail'; @@ -31,7 +21,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO const ACTION_EMAIL_REASSIGN = 'email-reassign'; const ACTION_CHANGE_PASSWORD = 'change-password'; - const ACTION_CHANGE_USERNAME = 'change-username'; const ACTION_ENTER_HISEC = 'hisec-enter'; const ACTION_EXIT_HISEC = 'hisec-exit'; @@ -59,14 +48,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO pht('Login: Signed Required Legalpad Documents'), self::ACTION_LOGOUT => pht('Logout'), self::ACTION_RESET_PASSWORD => pht('Reset Password'), - self::ACTION_CREATE => pht('Create Account'), - self::ACTION_EDIT => pht('Edit Account'), - self::ACTION_ADMIN => pht('Add/Remove Administrator'), - self::ACTION_SYSTEM_AGENT => pht('Add/Remove System Agent'), - self::ACTION_MAILING_LIST => pht('Add/Remove Mailing List'), - self::ACTION_DISABLE => pht('Enable/Disable'), - self::ACTION_APPROVE => pht('Approve Registration'), - self::ACTION_DELETE => pht('Delete User'), self::ACTION_CONDUIT_CERTIFICATE => pht('Conduit: Read Certificate'), self::ACTION_CONDUIT_CERTIFICATE_FAILURE @@ -77,7 +58,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO self::ACTION_EMAIL_VERIFY => pht('Email: Verify'), self::ACTION_EMAIL_REASSIGN => pht('Email: Reassign'), self::ACTION_CHANGE_PASSWORD => pht('Change Password'), - self::ACTION_CHANGE_USERNAME => pht('Change Username'), self::ACTION_ENTER_HISEC => pht('Hisec: Enter'), self::ACTION_EXIT_HISEC => pht('Hisec: Exit'), self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'), diff --git a/src/applications/people/xaction/PhabricatorUserApproveTransaction.php b/src/applications/people/xaction/PhabricatorUserApproveTransaction.php index e458c5822c..77d58bebdf 100644 --- a/src/applications/people/xaction/PhabricatorUserApproveTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserApproveTransaction.php @@ -19,10 +19,6 @@ final class PhabricatorUserApproveTransaction public function applyExternalEffects($object, $value) { $user = $object; - $this->newUserLog(PhabricatorUserLog::ACTION_APPROVE) - ->setOldValue((bool)$user->getIsApproved()) - ->setNewValue((bool)$value) - ->save(); $actor = $this->getActor(); $title = pht( diff --git a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php index 7a8a1c7966..f259e78ee4 100644 --- a/src/applications/people/xaction/PhabricatorUserDisableTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserDisableTransaction.php @@ -17,13 +17,6 @@ final class PhabricatorUserDisableTransaction $object->setIsDisabled((int)$value); } - public function applyExternalEffects($object, $value) { - $this->newUserLog(PhabricatorUserLog::ACTION_DISABLE) - ->setOldValue((bool)$object->getIsDisabled()) - ->setNewValue((bool)$value) - ->save(); - } - public function getTitle() { $new = $this->getNewValue(); if ($new) { diff --git a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php index 1b561d3236..5499f5d8cb 100644 --- a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php @@ -17,15 +17,6 @@ final class PhabricatorUserEmpowerTransaction $object->setIsAdmin((int)$value); } - public function applyExternalEffects($object, $value) { - $user = $object; - - $this->newUserLog(PhabricatorUserLog::ACTION_ADMIN) - ->setOldValue($this->getOldValue()) - ->setNewValue($value) - ->save(); - } - public function validateTransactions($object, array $xactions) { $user = $object; $actor = $this->getActor(); diff --git a/src/applications/people/xaction/PhabricatorUserTransactionType.php b/src/applications/people/xaction/PhabricatorUserTransactionType.php index dcd45d480e..89392fd039 100644 --- a/src/applications/people/xaction/PhabricatorUserTransactionType.php +++ b/src/applications/people/xaction/PhabricatorUserTransactionType.php @@ -1,13 +1,4 @@ getActor(), - $this->getObject()->getPHID(), - $action); - } - -} + extends PhabricatorModularTransactionType {} diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index b436b76716..338b296335 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -24,11 +24,6 @@ final class PhabricatorUserUsernameTransaction $old_username = $this->getOldValue(); $new_username = $this->getNewValue(); - $this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME) - ->setOldValue($old_username) - ->setNewValue($new_username) - ->save(); - // The SSH key cache currently includes usernames, so dirty it. See T12554 // for discussion. PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache(); From cd449254256db7fee74821e7735f7d8b8b09fffa Mon Sep 17 00:00:00 2001 From: Arturas Moskvinas Date: Mon, 22 Jul 2019 13:03:16 +0300 Subject: [PATCH 41/54] Allow users with no CAN_EDIT permissions to silence projects if they want to Summary: Humble user cannot silence/mute project if he/she has no CAN_EDIT permissions in it. You can actually leave it but if project is locked - then you're scr*wed. Test Plan: 1. On a testing phabricator instance created a dummy project 2. Changed that project permissions CAN_EDIT to be by admin only 3. Added poor soul with no CAN_EDIT permissions 4. Logged it in with poor soul 5. Tried to silence the project 6. The Project is successfully silenced 7. User is happy :) Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, Pawka Differential Revision: https://secure.phabricator.com/D20675 --- .../editor/PhabricatorApplicationTransactionEditor.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index da5e4d3634..01294e308a 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1801,6 +1801,11 @@ abstract class PhabricatorApplicationTransactionEditor // you don't need permissions. If you can eventually mute an object // for other users, this would need to be revisited. return null; + case PhabricatorProjectSilencedEdgeType::EDGECONST: + // At time of writing, you can only write this edge for yourself, so + // you don't need permissions. If you can eventually silence project + // for other users, this would need to be revisited. + return null; case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return null; case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: From 6831ed94faf6993b022387e64f3cb605bf92bbc8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Jul 2019 16:01:32 -0700 Subject: [PATCH 42/54] Contain fallout from overheating feed queries on user profile pages Summary: Fixes T13349. If the user profile page feed query overheats, it currently takes the whole page with it. Contain the blast to a smaller radius. Test Plan: {F6633322} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13349 Differential Revision: https://secure.phabricator.com/D20678 --- ...PhabricatorPeopleProfileViewController.php | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index b5c0e2b816..b929d980d5 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -252,15 +252,30 @@ final class PhabricatorPeopleProfileViewController PhabricatorUser $user, $viewer) { - $query = new PhabricatorFeedQuery(); - $query->withFilterPHIDs( - array( - $user->getPHID(), - )); - $query->setLimit(100); - $query->setViewer($viewer); + $query = id(new PhabricatorFeedQuery()) + ->setViewer($viewer) + ->withFilterPHIDs(array($user->getPHID())) + ->setLimit(100) + ->setReturnPartialResultsOnOverheat(true); + $stories = $query->execute(); + $overheated_view = null; + $is_overheated = $query->getIsOverheated(); + if ($is_overheated) { + $overheated_message = + PhabricatorApplicationSearchController::newOverheatedError( + (bool)$stories); + + $overheated_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setTitle(pht('Query Overheated')) + ->setErrors( + array( + $overheated_message, + )); + } + $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($viewer); $builder->setShowHovercards(true); @@ -268,8 +283,10 @@ final class PhabricatorPeopleProfileViewController 'requires but just a single step.')); $view = $builder->buildView(); - return $view->render(); - + return array( + $overheated_view, + $view->render(), + ); } } From 32dd13d43421b474db665224c9557386c352846a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 13:55:19 -0700 Subject: [PATCH 43/54] Modularize user activity log message types Summary: Depends on D20670. Ref T13343. The user activity message log types are currently hard-coded, so only upstream code can really use the log construct. Under the theory that we're going to keep this log around going forward (just focus it a little bit), modularize things so the log is extensible. Test Plan: Grepped for `UserLog::`, viewed activity logs in People and Settings. (If I missed something here -- say, misspelled a constant -- the effect should just be that older logs don't get a human-readable label, so stakes are very low.) Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20671 --- src/__phutil_library_map__.php | 42 ++++++++++++++ .../engine/PhabricatorAuthSessionEngine.php | 16 +++--- .../PhabricatorPasswordAuthProvider.php | 2 +- .../ConduitGetCertificateConduitAPIMethod.php | 6 +- .../people/editor/PhabricatorUserEditor.php | 12 ++-- .../PhabricatorPeopleLogSearchEngine.php | 10 +++- .../people/storage/PhabricatorUserLog.php | 55 ------------------- .../PhabricatorAddEmailUserLogType.php | 12 ++++ .../PhabricatorAddMultifactorUserLogType.php | 12 ++++ .../PhabricatorChangePasswordUserLogType.php | 12 ++++ ...orConduitCertificateFailureUserLogType.php | 12 ++++ ...abricatorConduitCertificateUserLogType.php | 12 ++++ .../PhabricatorEnterHisecUserLogType.php | 12 ++++ .../PhabricatorExitHisecUserLogType.php | 12 ++++ .../PhabricatorFailHisecUserLogType.php | 12 ++++ .../PhabricatorFullLoginUserLogType.php | 12 ++++ .../PhabricatorLoginFailureUserLogType.php | 12 ++++ .../userlog/PhabricatorLoginUserLogType.php | 12 ++++ .../userlog/PhabricatorLogoutUserLogType.php | 12 ++++ .../PhabricatorPartialLoginUserLogType.php | 12 ++++ .../PhabricatorPrimaryEmailUserLogType.php | 12 ++++ .../PhabricatorReassignEmailUserLogType.php | 12 ++++ .../PhabricatorRemoveEmailUserLogType.php | 12 ++++ ...habricatorRemoveMultifactorUserLogType.php | 12 ++++ .../PhabricatorResetPasswordUserLogType.php | 12 ++++ .../PhabricatorSignDocumentsUserLogType.php | 12 ++++ .../people/userlog/PhabricatorUserLogType.php | 19 +++++++ .../PhabricatorVerifyEmailUserLogType.php | 12 ++++ .../people/view/PhabricatorUserLogView.php | 6 +- .../PhabricatorMultiFactorSettingsPanel.php | 4 +- 30 files changed, 332 insertions(+), 80 deletions(-) create mode 100644 src/applications/people/userlog/PhabricatorAddEmailUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorAddMultifactorUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorChangePasswordUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorConduitCertificateFailureUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorConduitCertificateUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorEnterHisecUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorExitHisecUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorFailHisecUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorFullLoginUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorLoginFailureUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorLoginUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorLogoutUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorPartialLoginUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorReassignEmailUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorRemoveEmailUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorRemoveMultifactorUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorResetPasswordUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorSignDocumentsUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorUserLogType.php create mode 100644 src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a82181acd7..a00b8e35c7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2117,6 +2117,8 @@ phutil_register_library_map(array( 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', + 'PhabricatorAddEmailUserLogType' => 'applications/people/userlog/PhabricatorAddEmailUserLogType.php', + 'PhabricatorAddMultifactorUserLogType' => 'applications/people/userlog/PhabricatorAddMultifactorUserLogType.php', 'PhabricatorAdministratorsPolicyRule' => 'applications/people/policyrule/PhabricatorAdministratorsPolicyRule.php', 'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php', 'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php', @@ -2668,6 +2670,7 @@ phutil_register_library_map(array( 'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php', 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', + 'PhabricatorChangePasswordUserLogType' => 'applications/people/userlog/PhabricatorChangePasswordUserLogType.php', 'PhabricatorChangesetCachePurger' => 'applications/cache/purger/PhabricatorChangesetCachePurger.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', @@ -2721,7 +2724,9 @@ phutil_register_library_map(array( 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php', 'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php', + 'PhabricatorConduitCertificateFailureUserLogType' => 'applications/people/userlog/PhabricatorConduitCertificateFailureUserLogType.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', + 'PhabricatorConduitCertificateUserLogType' => 'applications/people/userlog/PhabricatorConduitCertificateUserLogType.php', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php', 'PhabricatorConduitContentSource' => 'infrastructure/contentsource/PhabricatorConduitContentSource.php', 'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php', @@ -3224,6 +3229,7 @@ phutil_register_library_map(array( 'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php', 'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php', 'PhabricatorEmptyQueryException' => 'infrastructure/query/exception/PhabricatorEmptyQueryException.php', + 'PhabricatorEnterHisecUserLogType' => 'applications/people/userlog/PhabricatorEnterHisecUserLogType.php', 'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php', 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', @@ -3236,6 +3242,7 @@ phutil_register_library_map(array( 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', + 'PhabricatorExitHisecUserLogType' => 'applications/people/userlog/PhabricatorExitHisecUserLogType.php', 'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php', 'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php', 'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php', @@ -3276,6 +3283,7 @@ phutil_register_library_map(array( 'PhabricatorFactObjectDimension' => 'applications/fact/storage/PhabricatorFactObjectDimension.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php', + 'PhabricatorFailHisecUserLogType' => 'applications/people/userlog/PhabricatorFailHisecUserLogType.php', 'PhabricatorFaviconRef' => 'applications/files/favicon/PhabricatorFaviconRef.php', 'PhabricatorFaviconRefQuery' => 'applications/files/favicon/PhabricatorFaviconRefQuery.php', 'PhabricatorFavoritesApplication' => 'applications/favorites/application/PhabricatorFavoritesApplication.php', @@ -3410,6 +3418,7 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', 'PhabricatorFlagsApplication' => 'applications/flag/application/PhabricatorFlagsApplication.php', 'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php', + 'PhabricatorFullLoginUserLogType' => 'applications/people/userlog/PhabricatorFullLoginUserLogType.php', 'PhabricatorFulltextEngine' => 'applications/search/index/PhabricatorFulltextEngine.php', 'PhabricatorFulltextEngineExtension' => 'applications/search/index/PhabricatorFulltextEngineExtension.php', 'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php', @@ -3548,7 +3557,10 @@ phutil_register_library_map(array( 'PhabricatorLockLogManagementWorkflow' => 'applications/daemon/management/PhabricatorLockLogManagementWorkflow.php', 'PhabricatorLockManagementWorkflow' => 'applications/daemon/management/PhabricatorLockManagementWorkflow.php', 'PhabricatorLogTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorLogTriggerAction.php', + 'PhabricatorLoginFailureUserLogType' => 'applications/people/userlog/PhabricatorLoginFailureUserLogType.php', + 'PhabricatorLoginUserLogType' => 'applications/people/userlog/PhabricatorLoginUserLogType.php', 'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php', + 'PhabricatorLogoutUserLogType' => 'applications/people/userlog/PhabricatorLogoutUserLogType.php', 'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php', 'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php', 'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php', @@ -3957,6 +3969,7 @@ phutil_register_library_map(array( 'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php', 'PhabricatorPackagesView' => 'applications/packages/view/PhabricatorPackagesView.php', 'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php', + 'PhabricatorPartialLoginUserLogType' => 'applications/people/userlog/PhabricatorPartialLoginUserLogType.php', 'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php', 'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php', 'PhabricatorPasswordDestructionEngineExtension' => 'applications/auth/extension/PhabricatorPasswordDestructionEngineExtension.php', @@ -4145,6 +4158,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php', + 'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php', 'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php', 'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php', 'PhabricatorProfileMenuEngine' => 'applications/search/engine/PhabricatorProfileMenuEngine.php', @@ -4368,6 +4382,7 @@ phutil_register_library_map(array( 'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php', 'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php', 'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php', + 'PhabricatorReassignEmailUserLogType' => 'applications/people/userlog/PhabricatorReassignEmailUserLogType.php', 'PhabricatorRebuildIndexesWorker' => 'applications/search/worker/PhabricatorRebuildIndexesWorker.php', 'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', @@ -4386,6 +4401,8 @@ phutil_register_library_map(array( 'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php', 'PhabricatorRemarkupHyperlinkEngineExtension' => 'applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php', 'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php', + 'PhabricatorRemoveEmailUserLogType' => 'applications/people/userlog/PhabricatorRemoveEmailUserLogType.php', + 'PhabricatorRemoveMultifactorUserLogType' => 'applications/people/userlog/PhabricatorRemoveMultifactorUserLogType.php', 'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php', 'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php', 'PhabricatorRepositoryActivateTransaction' => 'applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php', @@ -4524,6 +4541,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryVCSTransaction' => 'applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php', 'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php', 'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php', + 'PhabricatorResetPasswordUserLogType' => 'applications/people/userlog/PhabricatorResetPasswordUserLogType.php', 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', 'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', @@ -4630,6 +4648,7 @@ phutil_register_library_map(array( 'PhabricatorShiftChartFunction' => 'applications/fact/chart/PhabricatorShiftChartFunction.php', 'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php', 'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php', + 'PhabricatorSignDocumentsUserLogType' => 'applications/people/userlog/PhabricatorSignDocumentsUserLogType.php', 'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php', 'PhabricatorSinChartFunction' => 'applications/fact/chart/PhabricatorSinChartFunction.php', 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', @@ -4920,6 +4939,7 @@ phutil_register_library_map(array( 'PhabricatorUserFulltextEngine' => 'applications/people/search/PhabricatorUserFulltextEngine.php', 'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', + 'PhabricatorUserLogType' => 'applications/people/userlog/PhabricatorUserLogType.php', 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php', 'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php', @@ -4950,6 +4970,7 @@ phutil_register_library_map(array( 'PhabricatorUsersPolicyRule' => 'applications/people/policyrule/PhabricatorUsersPolicyRule.php', 'PhabricatorUsersSearchField' => 'applications/people/searchfield/PhabricatorUsersSearchField.php', 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', + 'PhabricatorVerifyEmailUserLogType' => 'applications/people/userlog/PhabricatorVerifyEmailUserLogType.php', 'PhabricatorVersionedDraft' => 'applications/draft/storage/PhabricatorVersionedDraft.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', 'PhabricatorVideoDocumentEngine' => 'applications/files/document/PhabricatorVideoDocumentEngine.php', @@ -8051,6 +8072,8 @@ phutil_register_library_map(array( 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', + 'PhabricatorAddEmailUserLogType' => 'PhabricatorUserLogType', + 'PhabricatorAddMultifactorUserLogType' => 'PhabricatorUserLogType', 'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorAlmanacApplication' => 'PhabricatorApplication', @@ -8716,6 +8739,7 @@ phutil_register_library_map(array( 'PhabricatorCelerityApplication' => 'PhabricatorApplication', 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', + 'PhabricatorChangePasswordUserLogType' => 'PhabricatorUserLogType', 'PhabricatorChangesetCachePurger' => 'PhabricatorCachePurger', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChartAxis' => 'Phobject', @@ -8774,7 +8798,9 @@ phutil_register_library_map(array( 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitApplication' => 'PhabricatorApplication', 'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow', + 'PhabricatorConduitCertificateFailureUserLogType' => 'PhabricatorUserLogType', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', + 'PhabricatorConduitCertificateUserLogType' => 'PhabricatorUserLogType', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitContentSource' => 'PhabricatorContentSource', 'PhabricatorConduitController' => 'PhabricatorController', @@ -9329,6 +9355,7 @@ phutil_register_library_map(array( 'PhabricatorEmojiRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorEmojiTranslation' => 'PhutilTranslation', 'PhabricatorEmptyQueryException' => 'Exception', + 'PhabricatorEnterHisecUserLogType' => 'PhabricatorUserLogType', 'PhabricatorEnumConfigType' => 'PhabricatorTextConfigType', 'PhabricatorEnv' => 'Phobject', 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', @@ -9341,6 +9368,7 @@ phutil_register_library_map(array( 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', 'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', + 'PhabricatorExitHisecUserLogType' => 'PhabricatorUserLogType', 'PhabricatorExportEngine' => 'Phobject', 'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType', 'PhabricatorExportEngineExtension' => 'Phobject', @@ -9386,6 +9414,7 @@ phutil_register_library_map(array( 'PhabricatorFactObjectDimension' => 'PhabricatorFactDimension', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', + 'PhabricatorFailHisecUserLogType' => 'PhabricatorUserLogType', 'PhabricatorFaviconRef' => 'Phobject', 'PhabricatorFaviconRefQuery' => 'Phobject', 'PhabricatorFavoritesApplication' => 'PhabricatorApplication', @@ -9557,6 +9586,7 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', 'PhabricatorFlagsApplication' => 'PhabricatorApplication', 'PhabricatorFlagsUIEventListener' => 'PhabricatorEventListener', + 'PhabricatorFullLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorFulltextEngine' => 'Phobject', 'PhabricatorFulltextEngineExtension' => 'Phobject', 'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule', @@ -9702,7 +9732,10 @@ phutil_register_library_map(array( 'PhabricatorLockLogManagementWorkflow' => 'PhabricatorLockManagementWorkflow', 'PhabricatorLockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLogTriggerAction' => 'PhabricatorTriggerAction', + 'PhabricatorLoginFailureUserLogType' => 'PhabricatorUserLogType', + 'PhabricatorLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorLogoutController' => 'PhabricatorAuthController', + 'PhabricatorLogoutUserLogType' => 'PhabricatorUserLogType', 'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorMacroApplication' => 'PhabricatorApplication', 'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType', @@ -10182,6 +10215,7 @@ phutil_register_library_map(array( 'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController', 'PhabricatorPackagesView' => 'AphrontView', 'PhabricatorPagerUIExample' => 'PhabricatorUIExample', + 'PhabricatorPartialLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorPassphraseApplication' => 'PhabricatorApplication', 'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorPasswordDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', @@ -10402,6 +10436,7 @@ phutil_register_library_map(array( ), 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 'PhabricatorPonderApplication' => 'PhabricatorApplication', + 'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine', 'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProfileMenuEngine' => 'Phobject', @@ -10669,6 +10704,7 @@ phutil_register_library_map(array( 'Iterator', ), 'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', + 'PhabricatorReassignEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorRebuildIndexesWorker' => 'PhabricatorWorker', 'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorRedirectController' => 'PhabricatorController', @@ -10687,6 +10723,8 @@ phutil_register_library_map(array( 'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', 'PhabricatorRemarkupHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', 'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample', + 'PhabricatorRemoveEmailUserLogType' => 'PhabricatorUserLogType', + 'PhabricatorRemoveMultifactorUserLogType' => 'PhabricatorUserLogType', 'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorRepository' => array( 'PhabricatorRepositoryDAO', @@ -10894,6 +10932,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryVCSTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO', 'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler', + 'PhabricatorResetPasswordUserLogType' => 'PhabricatorUserLogType', 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', @@ -11002,6 +11041,7 @@ phutil_register_library_map(array( 'PhabricatorShiftChartFunction' => 'PhabricatorChartFunction', 'PhabricatorShortSite' => 'PhabricatorSite', 'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', + 'PhabricatorSignDocumentsUserLogType' => 'PhabricatorUserLogType', 'PhabricatorSimpleEditType' => 'PhabricatorEditType', 'PhabricatorSinChartFunction' => 'PhabricatorChartFunction', 'PhabricatorSite' => 'AphrontSite', @@ -11336,6 +11376,7 @@ phutil_register_library_map(array( 'PhabricatorUserDAO', 'PhabricatorPolicyInterface', ), + 'PhabricatorUserLogType' => 'Phobject', 'PhabricatorUserLogView' => 'AphrontView', 'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType', @@ -11371,6 +11412,7 @@ phutil_register_library_map(array( 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorUsersSearchField' => 'PhabricatorSearchTokenizerField', 'PhabricatorVCSResponse' => 'AphrontResponse', + 'PhabricatorVerifyEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorVersionedDraft' => 'PhabricatorDraftDAO', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', 'PhabricatorVideoDocumentEngine' => 'PhabricatorDocumentEngine', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 7d73cb194d..7358a61a40 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -294,8 +294,8 @@ final class PhabricatorAuthSessionEngine extends Phobject { null, $identity_phid, ($partial - ? PhabricatorUserLog::ACTION_LOGIN_PARTIAL - : PhabricatorUserLog::ACTION_LOGIN)); + ? PhabricatorPartialLoginUserLogType::LOGTYPE + : PhabricatorLoginUserLogType::LOGTYPE)); $log->setDetails( array( @@ -366,7 +366,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $user, $user->getPHID(), - PhabricatorUserLog::ACTION_LOGOUT); + PhabricatorLogoutUserLogType::LOGTYPE); $log->save(); $extensions = PhabricatorAuthSessionEngineExtension::getAllExtensions(); @@ -688,13 +688,13 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_ENTER_HISEC); + PhabricatorEnterHisecUserLogType::LOGTYPE); $log->save(); } else { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_FAIL_HISEC); + PhabricatorFailHisecUserLogType::LOGTYPE); $log->save(); } } @@ -831,7 +831,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_EXIT_HISEC); + PhabricatorExitHisecUserLogType::LOGTYPE); $log->save(); } @@ -872,7 +872,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_LOGIN_FULL); + PhabricatorFullLoginUserLogType::LOGTYPE); $log->save(); unset($unguarded); } @@ -917,7 +917,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { $log = PhabricatorUserLog::initializeNewLog( $viewer, $viewer->getPHID(), - PhabricatorUserLog::ACTION_LOGIN_LEGALPAD); + PhabricatorSignDocumentsUserLogType::LOGTYPE); $log->save(); } unset($unguarded); diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index 215d316b3b..6b2681eea8 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -318,7 +318,7 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { $log = PhabricatorUserLog::initializeNewLog( null, $log_user ? $log_user->getPHID() : null, - PhabricatorUserLog::ACTION_LOGIN_FAILURE); + PhabricatorLoginFailureUserLogType::LOGTYPE); $log->save(); } diff --git a/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php b/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php index 6248b5a6ba..41716748e0 100644 --- a/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitGetCertificateConduitAPIMethod.php @@ -41,7 +41,7 @@ final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod { protected function execute(ConduitAPIRequest $request) { $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( - PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE, + PhabricatorConduitCertificateFailureUserLogType::LOGTYPE, 60 * 5); if (count($failed_attempts) > 5) { @@ -61,7 +61,7 @@ final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod { $log = PhabricatorUserLog::initializeNewLog( $request->getUser(), $info->getUserPHID(), - PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE) + PhabricatorConduitCertificateUserLogType::LOGTYPE) ->save(); } @@ -85,7 +85,7 @@ final class ConduitGetCertificateConduitAPIMethod extends ConduitAPIMethod { $log = PhabricatorUserLog::initializeNewLog( $request->getUser(), $info ? $info->getUserPHID() : '-', - PhabricatorUserLog::ACTION_CONDUIT_CERTIFICATE_FAILURE) + PhabricatorConduitCertificateFailureUserLogType::LOGTYPE) ->save(); } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 9ab2c9d60b..81f427ada8 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -78,7 +78,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_REASSIGN); + PhabricatorReassignEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); } @@ -195,7 +195,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_ADD); + PhabricatorAddEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); @@ -246,7 +246,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_REMOVE); + PhabricatorRemoveEmailUserLogType::LOGTYPE); $log->setOldValue($email->getAddress()); $log->save(); @@ -312,7 +312,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_PRIMARY); + PhabricatorPrimaryEmailUserLogType::LOGTYPE); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); @@ -371,7 +371,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_VERIFY); + PhabricatorVerifyEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); } @@ -433,7 +433,7 @@ final class PhabricatorUserEditor extends PhabricatorEditor { $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), - PhabricatorUserLog::ACTION_EMAIL_REASSIGN); + PhabricatorReassignEmailUserLogType::LOGTYPE); $log->setNewValue($email->getAddress()); $log->save(); } diff --git a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php index b052456cd3..ad37395b88 100644 --- a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php @@ -64,6 +64,9 @@ final class PhabricatorPeopleLogSearchEngine } protected function buildCustomSearchFields() { + $types = PhabricatorUserLogType::getAllLogTypes(); + $types = mpull($types, 'getLogTypeName', 'getLogTypeKey'); + return array( id(new PhabricatorUsersSearchField()) ->setKey('userPHIDs') @@ -79,7 +82,7 @@ final class PhabricatorPeopleLogSearchEngine ->setKey('actions') ->setLabel(pht('Actions')) ->setDescription(pht('Search for particular types of activity.')) - ->setOptions(PhabricatorUserLog::getActionTypeMap()), + ->setOptions($types), id(new PhabricatorSearchTextField()) ->setKey('ip') ->setLabel(pht('Filter IP')) @@ -194,7 +197,8 @@ final class PhabricatorPeopleLogSearchEngine } $handles = $viewer->loadHandles($phids); - $action_map = PhabricatorUserLog::getActionTypeMap(); + $types = PhabricatorUserLogType::getAllLogTypes(); + $types = mpull($types, 'getLogTypeName', 'getLogTypeKey'); $export = array(); foreach ($logs as $log) { @@ -214,7 +218,7 @@ final class PhabricatorPeopleLogSearchEngine } $action = $log->getAction(); - $action_name = idx($action_map, $action, pht('Unknown ("%s")', $action)); + $action_name = idx($types, $action, pht('Unknown ("%s")', $action)); $map = array( 'actorPHID' => $actor_phid, diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index c7550ba8e5..d433ebca07 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -3,32 +3,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO implements PhabricatorPolicyInterface { - const ACTION_LOGIN = 'login'; - const ACTION_LOGIN_PARTIAL = 'login-partial'; - const ACTION_LOGIN_FULL = 'login-full'; - const ACTION_LOGOUT = 'logout'; - const ACTION_LOGIN_FAILURE = 'login-fail'; - const ACTION_LOGIN_LEGALPAD = 'login-legalpad'; - const ACTION_RESET_PASSWORD = 'reset-pass'; - - const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert'; - const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail'; - - const ACTION_EMAIL_PRIMARY = 'email-primary'; - const ACTION_EMAIL_REMOVE = 'email-remove'; - const ACTION_EMAIL_ADD = 'email-add'; - const ACTION_EMAIL_VERIFY = 'email-verify'; - const ACTION_EMAIL_REASSIGN = 'email-reassign'; - - const ACTION_CHANGE_PASSWORD = 'change-password'; - - const ACTION_ENTER_HISEC = 'hisec-enter'; - const ACTION_EXIT_HISEC = 'hisec-exit'; - const ACTION_FAIL_HISEC = 'hisec-fail'; - - const ACTION_MULTI_ADD = 'multi-add'; - const ACTION_MULTI_REMOVE = 'multi-remove'; - protected $actorPHID; protected $userPHID; protected $action; @@ -38,35 +12,6 @@ final class PhabricatorUserLog extends PhabricatorUserDAO protected $remoteAddr; protected $session; - public static function getActionTypeMap() { - return array( - self::ACTION_LOGIN => pht('Login'), - self::ACTION_LOGIN_PARTIAL => pht('Login: Partial Login'), - self::ACTION_LOGIN_FULL => pht('Login: Upgrade to Full'), - self::ACTION_LOGIN_FAILURE => pht('Login: Failure'), - self::ACTION_LOGIN_LEGALPAD => - pht('Login: Signed Required Legalpad Documents'), - self::ACTION_LOGOUT => pht('Logout'), - self::ACTION_RESET_PASSWORD => pht('Reset Password'), - self::ACTION_CONDUIT_CERTIFICATE - => pht('Conduit: Read Certificate'), - self::ACTION_CONDUIT_CERTIFICATE_FAILURE - => pht('Conduit: Read Certificate Failure'), - self::ACTION_EMAIL_PRIMARY => pht('Email: Change Primary'), - self::ACTION_EMAIL_ADD => pht('Email: Add Address'), - self::ACTION_EMAIL_REMOVE => pht('Email: Remove Address'), - self::ACTION_EMAIL_VERIFY => pht('Email: Verify'), - self::ACTION_EMAIL_REASSIGN => pht('Email: Reassign'), - self::ACTION_CHANGE_PASSWORD => pht('Change Password'), - self::ACTION_ENTER_HISEC => pht('Hisec: Enter'), - self::ACTION_EXIT_HISEC => pht('Hisec: Exit'), - self::ACTION_FAIL_HISEC => pht('Hisec: Failed Attempt'), - self::ACTION_MULTI_ADD => pht('Multi-Factor: Add Factor'), - self::ACTION_MULTI_REMOVE => pht('Multi-Factor: Remove Factor'), - ); - } - - public static function initializeNewLog( PhabricatorUser $actor = null, $object_phid = null, diff --git a/src/applications/people/userlog/PhabricatorAddEmailUserLogType.php b/src/applications/people/userlog/PhabricatorAddEmailUserLogType.php new file mode 100644 index 0000000000..5587f46ed3 --- /dev/null +++ b/src/applications/people/userlog/PhabricatorAddEmailUserLogType.php @@ -0,0 +1,12 @@ +getPhobjectClassConstant('LOGTYPE', 32); + } + + abstract public function getLogTypeName(); + + final public static function getAllLogTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getLogTypeKey') + ->execute(); + } + +} diff --git a/src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php b/src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php new file mode 100644 index 0000000000..b6f39a2e7b --- /dev/null +++ b/src/applications/people/userlog/PhabricatorVerifyEmailUserLogType.php @@ -0,0 +1,12 @@ +loadHandles($phids); - $action_map = PhabricatorUserLog::getActionTypeMap(); + $types = PhabricatorUserLogType::getAllLogTypes(); + $types = mpull($types, 'getLogTypeName', 'getLogTypeKey'); + $base_uri = $this->searchBaseURI; $viewer_phid = $viewer->getPHID(); @@ -69,7 +71,7 @@ final class PhabricatorUserLogView extends AphrontView { } $action = $log->getAction(); - $action_name = idx($action_map, $action, $action); + $action_name = idx($types, $action, $action); if ($actor_phid) { $actor_name = $handles[$actor_phid]->renderLink(); diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index abbb88c0a5..0054610c28 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -315,7 +315,7 @@ final class PhabricatorMultiFactorSettingsPanel $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), - PhabricatorUserLog::ACTION_MULTI_ADD); + PhabricatorAddMultifactorUserLogType::LOGTYPE); $log->save(); $user->updateMultiFactorEnrollment(); @@ -423,7 +423,7 @@ final class PhabricatorMultiFactorSettingsPanel $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), - PhabricatorUserLog::ACTION_MULTI_REMOVE); + PhabricatorRemoveMultifactorUserLogType::LOGTYPE); $log->save(); $user->updateMultiFactorEnrollment(); From 57799bc82bad8dd96e4a7751bf635f3801490f0f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 14:47:29 -0700 Subject: [PATCH 44/54] Give user log types a tokenizer and datasource instead of a page of checkboxes Summary: Depends on D20671. Ref T13343. Now that log types are modular, provide a datasource/tokenizer for selecting them since we already have a lot (even after I purged a few in D20670) and I'm planning to add at least one more ("Request password reset"). Test Plan: {F6608534} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20672 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPeopleLogSearchEngine.php | 4 +- .../PhabricatorUserLogTypeDatasource.php | 43 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a00b8e35c7..45d31361d0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4940,6 +4940,7 @@ phutil_register_library_map(array( 'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', 'PhabricatorUserLogType' => 'applications/people/userlog/PhabricatorUserLogType.php', + 'PhabricatorUserLogTypeDatasource' => 'applications/people/typeahead/PhabricatorUserLogTypeDatasource.php', 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php', 'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php', @@ -11377,6 +11378,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhabricatorUserLogType' => 'Phobject', + 'PhabricatorUserLogTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorUserLogView' => 'AphrontView', 'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType', 'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType', diff --git a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php index ad37395b88..7e7fca1cd4 100644 --- a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php @@ -78,11 +78,11 @@ final class PhabricatorPeopleLogSearchEngine ->setAliases(array('actors', 'actor', 'actorPHID')) ->setLabel(pht('Actors')) ->setDescription(pht('Search for activity by specific users.')), - id(new PhabricatorSearchCheckboxesField()) + id(new PhabricatorSearchDatasourceField()) ->setKey('actions') ->setLabel(pht('Actions')) ->setDescription(pht('Search for particular types of activity.')) - ->setOptions($types), + ->setDatasource(new PhabricatorUserLogTypeDatasource()), id(new PhabricatorSearchTextField()) ->setKey('ip') ->setLabel(pht('Filter IP')) diff --git a/src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php b/src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php new file mode 100644 index 0000000000..39241a020c --- /dev/null +++ b/src/applications/people/typeahead/PhabricatorUserLogTypeDatasource.php @@ -0,0 +1,43 @@ +buildResults(); + return $this->filterResultsAgainstTokens($results); + } + + protected function renderSpecialTokens(array $values) { + return $this->renderTokensFromResults($this->buildResults(), $values); + } + + private function buildResults() { + $results = array(); + + $type_map = PhabricatorUserLogType::getAllLogTypes(); + foreach ($type_map as $type_key => $type) { + + $result = id(new PhabricatorTypeaheadResult()) + ->setPHID($type_key) + ->setName($type->getLogTypeName()); + + $results[$type_key] = $result; + } + + return $results; + } + +} From 60db658d52c4a51173c0319f55690a35d473b1b2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 19 Jul 2019 14:59:36 -0700 Subject: [PATCH 45/54] Record account recovery email links in the user activity log and make the mail message reference the log Summary: Depends on D20672. Ref T13343. When a user requests an account access link via email: - log it in the activity log; and - reference the log in the mail. This makes it easier to ban users misusing the feature, provided they're coming from a single remote address, and takes a few steps down the pathway toward a button in the mail that users can click to report the action, suspend account recovery for their account, etc. Test Plan: - Requested an email recovery link. - Saw request appear in the user activity log. - Saw a reference to the log entry in the mail footer. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20673 --- src/__phutil_library_map__.php | 2 ++ .../PhabricatorEmailLoginController.php | 8 ++++++- .../mail/PhabricatorPeopleMailEngine.php | 22 +++++++++++++++++++ .../PhabricatorEmailLoginUserLogType.php | 12 ++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/applications/people/userlog/PhabricatorEmailLoginUserLogType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 45d31361d0..bee0bb7c38 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3216,6 +3216,7 @@ phutil_register_library_map(array( 'PhabricatorEmailFormatSetting' => 'applications/settings/setting/PhabricatorEmailFormatSetting.php', 'PhabricatorEmailFormatSettingsPanel' => 'applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php', 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', + 'PhabricatorEmailLoginUserLogType' => 'applications/people/userlog/PhabricatorEmailLoginUserLogType.php', 'PhabricatorEmailNotificationsSetting' => 'applications/settings/setting/PhabricatorEmailNotificationsSetting.php', 'PhabricatorEmailPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php', 'PhabricatorEmailRePrefixSetting' => 'applications/settings/setting/PhabricatorEmailRePrefixSetting.php', @@ -9343,6 +9344,7 @@ phutil_register_library_map(array( 'PhabricatorEmailFormatSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailFormatSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', + 'PhabricatorEmailLoginUserLogType' => 'PhabricatorUserLogType', 'PhabricatorEmailNotificationsSetting' => 'PhabricatorSelectSetting', 'PhabricatorEmailPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEmailRePrefixSetting' => 'PhabricatorSelectSetting', diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index 492526edf5..a744a90a6c 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -104,10 +104,16 @@ final class PhabricatorEmailLoginController if (!$errors) { $target_address = new PhutilEmailAddress($target_email->getAddress()); + $user_log = PhabricatorUserLog::initializeNewLog( + $viewer, + $target_user->getPHID(), + PhabricatorEmailLoginUserLogType::LOGTYPE); + $mail_engine = id(new PhabricatorPeopleEmailLoginMailEngine()) ->setSender($viewer) ->setRecipient($target_user) - ->setRecipientAddress($target_address); + ->setRecipientAddress($target_address) + ->setActivityLog($user_log); try { $mail_engine->validateMail(); diff --git a/src/applications/people/mail/PhabricatorPeopleMailEngine.php b/src/applications/people/mail/PhabricatorPeopleMailEngine.php index c1379dda9e..6b7aa7818e 100644 --- a/src/applications/people/mail/PhabricatorPeopleMailEngine.php +++ b/src/applications/people/mail/PhabricatorPeopleMailEngine.php @@ -6,6 +6,7 @@ abstract class PhabricatorPeopleMailEngine private $sender; private $recipient; private $recipientAddress; + private $activityLog; final public function setSender(PhabricatorUser $sender) { $this->sender = $sender; @@ -47,6 +48,15 @@ abstract class PhabricatorPeopleMailEngine return ($this->recipientAddress !== null); } + final public function setActivityLog(PhabricatorUserLog $activity_log) { + $this->activityLog = $activity_log; + return $this; + } + + final public function getActivityLog() { + return $this->activityLog; + } + final public function canSendMail() { try { $this->validateMail(); @@ -68,6 +78,18 @@ abstract class PhabricatorPeopleMailEngine $mail->addTos(array($recipient->getPHID())); } + $activity_log = $this->getActivityLog(); + if ($activity_log) { + $activity_log->save(); + + $body = array(); + $body[] = rtrim($mail->getBody(), "\n"); + $body[] = pht('Activity Log ID: #%d', $activity_log->getID()); + $body = implode("\n\n", $body)."\n"; + + $mail->setBody($body); + } + $mail ->setForceDelivery(true) ->save(); diff --git a/src/applications/people/userlog/PhabricatorEmailLoginUserLogType.php b/src/applications/people/userlog/PhabricatorEmailLoginUserLogType.php new file mode 100644 index 0000000000..1e49788990 --- /dev/null +++ b/src/applications/people/userlog/PhabricatorEmailLoginUserLogType.php @@ -0,0 +1,12 @@ + Date: Fri, 19 Jul 2019 15:24:37 -0700 Subject: [PATCH 46/54] Provide a basic detail view for user activity logs Summary: Depends on D20673. Ref T13343. Since we're now putting log IDs in email, make the UI a little better for working with log IDs. Some day, this page might have actions like "report this as suspicious" or whatever, but I'm not planning to do any of that for now. Test Plan: {F6608631} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13343 Differential Revision: https://secure.phabricator.com/D20674 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPeopleApplication.php | 1 + .../PhabricatorPeopleLogViewController.php | 92 +++++++++++++++++++ .../query/PhabricatorPeopleLogQuery.php | 13 +++ .../people/storage/PhabricatorUserLog.php | 37 ++++++++ .../people/view/PhabricatorUserLogView.php | 51 +++++----- 6 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 src/applications/people/controller/PhabricatorPeopleLogViewController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bee0bb7c38..0f687ca681 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4031,6 +4031,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', 'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php', + 'PhabricatorPeopleLogViewController' => 'applications/people/controller/PhabricatorPeopleLogViewController.php', 'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php', 'PhabricatorPeopleMailEngine' => 'applications/people/mail/PhabricatorPeopleMailEngine.php', 'PhabricatorPeopleMailEngineException' => 'applications/people/mail/PhabricatorPeopleMailEngineException.php', @@ -10291,6 +10292,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorPeopleLogViewController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', 'PhabricatorPeopleMailEngine' => 'Phobject', 'PhabricatorPeopleMailEngineException' => 'Exception', diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 9cc3607930..ec6892d022 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -44,6 +44,7 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { $this->getQueryRoutePattern() => 'PhabricatorPeopleListController', 'logs/' => array( $this->getQueryRoutePattern() => 'PhabricatorPeopleLogsController', + '(?P\d+)/' => 'PhabricatorPeopleLogViewController', ), 'invite/' => array( '(?:query/(?P[^/]+)/)?' diff --git a/src/applications/people/controller/PhabricatorPeopleLogViewController.php b/src/applications/people/controller/PhabricatorPeopleLogViewController.php new file mode 100644 index 0000000000..faaf4fd5ca --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleLogViewController.php @@ -0,0 +1,92 @@ +getViewer(); + $id = $request->getURIData('id'); + + $log = id(new PhabricatorPeopleLogQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$log) { + return new Aphront404Response(); + } + + $logs_uri = $this->getApplicationURI('logs/'); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Activity Logs'), $logs_uri) + ->addTextCrumb($log->getObjectName()) + ->setBorder(true); + + $header = $this->buildHeaderView($log); + $properties = $this->buildPropertiesView($log); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->addPropertySection(pht('Details'), $properties); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle($log->getObjectName()) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorUserLog $log) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($log->getObjectName()); + + return $view; + } + + private function buildPropertiesView(PhabricatorUserLog $log) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $type_map = PhabricatorUserLogType::getAllLogTypes(); + $type_map = mpull($type_map, 'getLogTypeName', 'getLogTypeKey'); + + $action = $log->getAction(); + $type_name = idx($type_map, $action, $action); + + $view->addProperty(pht('Event Type'), $type_name); + + $view->addProperty( + pht('Event Date'), + phabricator_datetime($log->getDateCreated(), $viewer)); + + $actor_phid = $log->getActorPHID(); + if ($actor_phid) { + $view->addProperty( + pht('Acting User'), + $viewer->renderHandle($actor_phid)); + } + + $user_phid = $log->getUserPHID(); + if ($user_phid) { + $view->addProperty( + pht('Affected User'), + $viewer->renderHandle($user_phid)); + } + + $remote_address = $log->getRemoteAddressForViewer($viewer); + if ($remote_address !== null) { + $view->addProperty(pht('Remote Address'), $remote_address); + } + + return $view; + } + +} diff --git a/src/applications/people/query/PhabricatorPeopleLogQuery.php b/src/applications/people/query/PhabricatorPeopleLogQuery.php index fc6a87b335..203f79579a 100644 --- a/src/applications/people/query/PhabricatorPeopleLogQuery.php +++ b/src/applications/people/query/PhabricatorPeopleLogQuery.php @@ -3,6 +3,7 @@ final class PhabricatorPeopleLogQuery extends PhabricatorCursorPagedPolicyAwareQuery { + private $ids; private $actorPHIDs; private $userPHIDs; private $relatedPHIDs; @@ -12,6 +13,11 @@ final class PhabricatorPeopleLogQuery private $dateCreatedMin; private $dateCreatedMax; + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withActorPHIDs(array $actor_phids) { $this->actorPHIDs = $actor_phids; return $this; @@ -59,6 +65,13 @@ final class PhabricatorPeopleLogQuery protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + if ($this->actorPHIDs !== null) { $where[] = qsprintf( $conn, diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index d433ebca07..61ccaff1ed 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -100,6 +100,43 @@ final class PhabricatorUserLog extends PhabricatorUserDAO ) + parent::getConfiguration(); } + public function getURI() { + return urisprintf('/people/logs/%s/', $this->getID()); + } + + public function getObjectName() { + return pht('Activity Log %d', $this->getID()); + } + + public function getRemoteAddressForViewer(PhabricatorUser $viewer) { + $viewer_phid = $viewer->getPHID(); + $actor_phid = $this->getActorPHID(); + $user_phid = $this->getUserPHID(); + + if (!$viewer_phid) { + $can_see_ip = false; + } else if ($viewer->getIsAdmin()) { + $can_see_ip = true; + } else if ($viewer_phid == $actor_phid) { + // You can see the address if you took the action. + $can_see_ip = true; + } else if (!$actor_phid && ($viewer_phid == $user_phid)) { + // You can see the address if it wasn't authenticated and applied + // to you (partial login). + $can_see_ip = true; + } else { + // You can't see the address when an administrator disables your + // account, since it's their address. + $can_see_ip = false; + } + + if (!$can_see_ip) { + return null; + } + + return $this->getRemoteAddr(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/people/view/PhabricatorUserLogView.php b/src/applications/people/view/PhabricatorUserLogView.php index 5c7e2ac1be..cf728af973 100644 --- a/src/applications/people/view/PhabricatorUserLogView.php +++ b/src/applications/people/view/PhabricatorUserLogView.php @@ -41,33 +41,16 @@ final class PhabricatorUserLogView extends AphrontView { $actor_phid = $log->getActorPHID(); $user_phid = $log->getUserPHID(); - if ($viewer->getIsAdmin()) { - $can_see_ip = true; - } else if ($viewer_phid == $actor_phid) { - // You can see the address if you took the action. - $can_see_ip = true; - } else if (!$actor_phid && ($viewer_phid == $user_phid)) { - // You can see the address if it wasn't authenticated and applied - // to you (partial login). - $can_see_ip = true; - } else { - // You can't see the address when an administrator disables your - // account, since it's their address. - $can_see_ip = false; - } - - if ($can_see_ip) { - $ip = $log->getRemoteAddr(); + $remote_address = $log->getRemoteAddressForViewer($viewer); + if ($remote_address !== null) { if ($base_uri) { - $ip = phutil_tag( + $remote_address = phutil_tag( 'a', array( - 'href' => $base_uri.'?ip='.$ip.'#R', + 'href' => $base_uri.'?ip='.$remote_address.'#R', ), - $ip); + $remote_address); } - } else { - $ip = null; } $action = $log->getAction(); @@ -85,37 +68,47 @@ final class PhabricatorUserLogView extends AphrontView { $user_name = null; } + $action_link = phutil_tag( + 'a', + array( + 'href' => $log->getURI(), + ), + $action_name); + $rows[] = array( - phabricator_date($log->getDateCreated(), $viewer), - phabricator_time($log->getDateCreated(), $viewer), - $action_name, + $log->getID(), + $action_link, $actor_name, $user_name, - $ip, + $remote_address, $session, + phabricator_date($log->getDateCreated(), $viewer), + phabricator_time($log->getDateCreated(), $viewer), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( - pht('Date'), - pht('Time'), + pht('ID'), pht('Action'), pht('Actor'), pht('User'), pht('IP'), pht('Session'), + pht('Date'), + pht('Time'), )); $table->setColumnClasses( array( '', - 'right', 'wide', '', '', '', 'n', + '', + 'right', )); return $table; From f6621a5fdcffa00feb41bb8b64a475bc5bcf2dc0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 24 Jul 2019 08:11:48 -0700 Subject: [PATCH 47/54] Tailor "Restart All Builds" for the complex realities of modern build restart rules Summary: Fixes T13348. Currently, the Harbormaster UI shows "Restart All Builds", but it really means "Restart Restartable Builds", which is often fewer than "All" builds (because of autobuilds, permissions, and/or configuration). Remove the misleading term "All" and make the workflow preview exactly which builds will and will not be affected, and why. Test Plan: {F6636313} {F6636314} {F6636315} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13348 Differential Revision: https://secure.phabricator.com/D20679 --- .../HarbormasterBuildableActionController.php | 157 +++++++++++++++--- .../HarbormasterBuildableViewController.php | 8 +- 2 files changed, 139 insertions(+), 26 deletions(-) diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index b3f60cd1dc..b701274eb0 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -24,26 +24,27 @@ final class HarbormasterBuildableActionController $issuable = array(); - foreach ($buildable->getBuilds() as $build) { + $builds = $buildable->getBuilds(); + foreach ($builds as $key => $build) { switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($build->canRestartBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($build->canPauseBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($build->canResumeBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; case HarbormasterBuildCommand::COMMAND_ABORT: if ($build->canAbortBuild()) { - $issuable[] = $build; + $issuable[$key] = $build; } break; default: @@ -59,6 +60,14 @@ final class HarbormasterBuildableActionController } } + $building = false; + foreach ($issuable as $key => $build) { + if ($build->isBuilding()) { + $building = true; + break; + } + } + $return_uri = '/'.$buildable->getMonogram(); if ($request->isDialogFormPost() && $issuable) { $editor = id(new HarbormasterBuildableTransactionEditor()) @@ -89,34 +98,137 @@ final class HarbormasterBuildableActionController return id(new AphrontRedirectResponse())->setURI($return_uri); } + $width = AphrontDialogView::WIDTH_DEFAULT; + switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: + // See T13348. The "Restart Builds" action may restart only a subset + // of builds, so show the user a preview of which builds will actually + // restart. + + $body = array(); + if ($issuable) { - $title = pht('Really restart builds?'); - - if ($restricted) { - $body = pht( - 'You only have permission to restart some builds. Progress '. - 'on builds you have permission to restart will be discarded '. - 'and they will restart. Side effects of these builds will '. - 'occur again. Really restart all builds?'); - } else { - $body = pht( - 'Progress on all builds will be discarded, and all builds will '. - 'restart. Side effects of the builds will occur again. Really '. - 'restart all builds?'); - } - + $title = pht('Restart Builds'); $submit = pht('Restart Builds'); } else { $title = pht('Unable to Restart Builds'); + } + + if ($builds) { + $width = AphrontDialogView::WIDTH_FORM; + + $body[] = pht('Builds for this buildable:'); + + $rows = array(); + foreach ($builds as $key => $build) { + if (isset($issuable[$key])) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-repeat green'); + $build_note = pht('Will Restart'); + } else { + $icon = null; + + try { + $build->assertCanRestartBuild(); + } catch (HarbormasterRestartException $ex) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-times red'); + $build_note = pht( + '%s: %s', + phutil_tag('strong', array(), pht('Not Restartable')), + $ex->getTitle()); + } + + if (!$icon) { + try { + $build->assertCanIssueCommand($viewer, $action); + } catch (PhabricatorPolicyException $ex) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-lock red'); + $build_note = pht( + '%s: %s', + phutil_tag('strong', array(), pht('Not Restartable')), + pht('You do not have permission to restart this build.')); + } + } + + if (!$icon) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-times red'); + $build_note = pht('Will Not Restart'); + } + } + + $build_name = phutil_tag( + 'a', + array( + 'href' => $build->getURI(), + 'target' => '_blank', + ), + pht('%s %s', $build->getObjectName(), $build->getName())); + + $rows[] = array( + $icon, + $build_name, + $build_note, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Build'), + pht('Action'), + )) + ->setColumnClasses( + array( + null, + 'pri', + 'wide', + )); + + $table = phutil_tag( + 'div', + array( + 'class' => 'mlt mlb', + ), + $table); + + $body[] = $table; + } + + if ($issuable) { + $warnings = array(); if ($restricted) { - $body = pht('You do not have permission to restart any builds.'); + $warnings[] = pht( + 'You only have permission to restart some builds.'); + } + + if ($building) { + $warnings[] = pht( + 'Progress on running builds will be discarded.'); + } + + $warnings[] = pht( + 'When a build is restarted, side effects associated with '. + 'the build may occur again.'); + + $body[] = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors($warnings); + + $body[] = pht('Really restart builds?'); + } else { + if ($restricted) { + $body[] = pht('You do not have permission to restart any builds.'); } else { - $body = pht('No builds can be restarted.'); + $body[] = pht('No builds can be restarted.'); } } + break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($issuable) { @@ -193,6 +305,7 @@ final class HarbormasterBuildableActionController $dialog = id(new AphrontDialogView()) ->setUser($viewer) + ->setWidth($width) ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index 40f6587116..aa433be656 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -128,7 +128,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-repeat') - ->setName(pht('Restart All Builds')) + ->setName(pht('Restart Builds')) ->setHref($this->getApplicationURI($restart_uri)) ->setWorkflow(true) ->setDisabled(!$can_restart || !$can_edit)); @@ -136,7 +136,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pause') - ->setName(pht('Pause All Builds')) + ->setName(pht('Pause Builds')) ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) ->setDisabled(!$can_pause || !$can_edit)); @@ -144,7 +144,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-play') - ->setName(pht('Resume All Builds')) + ->setName(pht('Resume Builds')) ->setHref($this->getApplicationURI($resume_uri)) ->setWorkflow(true) ->setDisabled(!$can_resume || !$can_edit)); @@ -152,7 +152,7 @@ final class HarbormasterBuildableViewController $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-exclamation-triangle') - ->setName(pht('Abort All Builds')) + ->setName(pht('Abort Builds')) ->setHref($this->getApplicationURI($abort_uri)) ->setWorkflow(true) ->setDisabled(!$can_abort || !$can_edit)); From 7e09da3313fb16b52fd21c7d8e8164073b3d3ad1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jul 2019 11:49:23 -0700 Subject: [PATCH 48/54] Fix policy behavior of "slowvote.info" API method Summary: Ref T13350. This ancient API method is missing modern policy checks. Test Plan: - Set visibility of vote X to "Only: epriestley". - Called "slowvote.info" as another user. - Before: retrieved poll title and author. - After: policy error. - Called "slowvote.info" on a visible poll, got information before and after. Maniphest Tasks: T13350 Differential Revision: https://secure.phabricator.com/D20684 --- .../slowvote/conduit/SlowvoteInfoConduitAPIMethod.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php index cecd799ad0..4041b1f70c 100644 --- a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php +++ b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php @@ -27,8 +27,14 @@ final class SlowvoteInfoConduitAPIMethod extends SlowvoteConduitAPIMethod { } protected function execute(ConduitAPIRequest $request) { + $viewer = $this->getViewer(); + $poll_id = $request->getValue('poll_id'); - $poll = id(new PhabricatorSlowvotePoll())->load($poll_id); + + $poll = id(new PhabricatorSlowvoteQuery()) + ->setViewer($viewer) + ->withIDs(array($poll_id)) + ->executeOne(); if (!$poll) { throw new ConduitException('ERR_BAD_POLL'); } From 7d41535010aa71408cf656e08f9d61fc940f1718 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 24 Jul 2019 10:14:22 -0700 Subject: [PATCH 49/54] When a task card is edited, emit update events for old boards and parent boards Summary: Ref T4900. When a card is edited, we currently emit an update notification for all the projects the task is tagged with. This isn't quite the right set: - We want to emit notifications for projects the task //was previously// tagged with, so it can be removed from boards it should no longer be part of. - We want to emit notifications for ancestors of projects the task is or was tagged with, so parent project boards can be updated. - However, we don't need to emit notifications for projects that don't actually have workboards. Adjust the notification set to align better to these rules. Test Plan: - Removal of Parent Project: Edited a task on board "A > B", removing the "B" project tag. Saw board A update in another window. - Normal Update: Edited a task title on board X, saw board X update in another window. - Used `bin/aphlict debug` to inspect the notification set, saw generally sensible-seeming data going over the wire. Reviewers: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20680 --- resources/celerity/map.php | 28 +++---- .../editor/ManiphestTransactionEditor.php | 76 ++++++++++++++----- .../client/PhabricatorNotificationClient.php | 4 + .../js/application/projects/WorkboardBoard.js | 2 +- 4 files changed, 78 insertions(+), 32 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 90ef62ed1b..ca09a081e5 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -412,7 +412,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '75727403', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'b46d88c5', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '84f82dad', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -743,7 +743,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '75727403', + 'javelin-workboard-board' => 'b46d88c5', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '84f82dad', 'javelin-workboard-column' => 'c3d24e63', @@ -1549,18 +1549,6 @@ return array( 'javelin-uri', 'javelin-request', ), - 75727403 => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '78bc5d94' => array( 'javelin-behavior', 'javelin-uri', @@ -1900,6 +1888,18 @@ return array( 'b347a301' => array( 'javelin-behavior', ), + 'b46d88c5' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), 'b49fd60c' => array( 'multirow-row-manager', 'trigger-rule', diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 235c73280b..247f5ce1fe 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -3,6 +3,7 @@ final class ManiphestTransactionEditor extends PhabricatorApplicationTransactionEditor { + private $oldProjectPHIDs; private $moreValidationErrors = array(); public function getEditorApplicationClass() { @@ -378,6 +379,11 @@ final class ManiphestTransactionEditor } } + $send_notifications = PhabricatorNotificationClient::isEnabled(); + if ($send_notifications) { + $this->oldProjectPHIDs = $this->loadProjectPHIDs($object); + } + return $results; } @@ -859,14 +865,61 @@ final class ManiphestTransactionEditor return array_values($phid_list); } - protected function didApplyTransactions($object, array $xactions) { - // TODO: This should include projects which the object was previously - // associated with but no longer is (so it can be removed from those - // boards) but currently does not. + $send_notifications = PhabricatorNotificationClient::isEnabled(); + if ($send_notifications) { + $old_phids = $this->oldProjectPHIDs; + $new_phids = $this->loadProjectPHIDs($object); + + // We want to emit update notifications for all old and new tagged + // projects, and all parents of those projects. For example, if an + // edit removes project "A > B" from a task, the "A" workboard should + // receive an update event. + + $project_phids = array_fuse($old_phids) + array_fuse($new_phids); + $project_phids = array_keys($project_phids); + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($project_phids) + ->execute(); + + $notify_projects = array(); + foreach ($projects as $project) { + $notify_projects[$project->getPHID()] = $project; + foreach ($project->getAncestorProjects() as $ancestor) { + $notify_projects[$ancestor->getPHID()] = $ancestor; + } + } + + foreach ($notify_projects as $key => $project) { + if (!$project->getHasWorkboard()) { + unset($notify_projects[$key]); + } + } + + $notify_phids = array_keys($notify_projects); + + if ($notify_phids) { + $data = array( + 'type' => 'workboards', + 'subscribers' => $notify_phids, + ); + + PhabricatorNotificationClient::tryToPostMessage($data); + } + } + + return $xactions; + } + + private function loadProjectPHIDs(ManiphestTask $task) { + if (!$task->getPHID()) { + return array(); + } $edge_query = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(array($object->getPHID())) + ->withSourcePHIDs(array($task->getPHID())) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, @@ -874,18 +927,7 @@ final class ManiphestTransactionEditor $edge_query->execute(); - $project_phids = $edge_query->getDestinationPHIDs(); - - if ($project_phids) { - $data = array( - 'type' => 'workboards', - 'subscribers' => $project_phids, - ); - - PhabricatorNotificationClient::tryToPostMessage($data); - } - - return $xactions; + return $edge_query->getDestinationPHIDs(); } } diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php index ff5538dbcf..1cede1498d 100644 --- a/src/applications/notification/client/PhabricatorNotificationClient.php +++ b/src/applications/notification/client/PhabricatorNotificationClient.php @@ -37,4 +37,8 @@ final class PhabricatorNotificationClient extends Phobject { } } + public static function isEnabled() { + return (bool)PhabricatorNotificationServerRef::getEnabledAdminServers(); + } + } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 2f904b9f68..cce0ed9f69 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -711,7 +711,7 @@ JX.install('WorkboardBoard', { // Compare the server state to the client state, and add or remove // cards on the client as necessary to synchronize them. - if (update_map[card_phid][column_phid]) { + if (update_map[card_phid] && update_map[card_phid][column_phid]) { if (!card) { column.newCard(card_phid); column.markForRedraw(); From d81d0c3ea099589e4717a2be8cfa794670d80568 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 24 Jul 2019 11:00:27 -0700 Subject: [PATCH 50/54] Fix an issue where editing cards on a workboard with implicit column ordering could reorder cards improperly Summary: Depends on D20680. Ref T4900. The "BoardLayoutEngine" operates on PHIDs without knowledge of the underlying objects, but this means it has to be sensitive to PHID input order when falling back to a default layout order. We use "default layout order" on workboards which are sorted by "Natual" order but which have one or more cards which no user has ever reordered. For example, if you add 10 tasks to a project, then create a board, there's no existing order for those tasks in the "Backlog" column. The layout engine uses the input order to place them in the column, with the expectation that input order is ID/creation order, so new cards will end up on top. I think this code never really made an explicit effort to guarantee that the LayoutEngine received objects in ID order, and it just sort of happened to by coincidence and good fortune. Some recent change has disrupted this, so the edit operation can end up with the PHIDs arranged in arbitrary order. Explicitly put them in ID order so we always get an implicit default layout order to fall back to. Also, update to `msortv()`. Test Plan: - Tagged several tasks with project X, a project without a board yet. - Created the project X workboard. - (Did not drag any tasks around on the project X board!) - Viewed the board in "Natural" order. This creates a view of the board where tasks are ordered by implicit/virtual/input order. The expectation, and "view" behavior of this board, is that this order is "newest on top". - Edited one of the cards on the board, changing the title (don't reorder it!) - Before: page state synchronized with cards in arbitrary/random/different order. - After: page state synchronized with cards in the same order as before ("newest on top"). Reviewers: amckinley Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20681 --- .../project/engine/PhabricatorBoardLayoutEngine.php | 6 +++--- .../project/engine/PhabricatorBoardResponseEngine.php | 9 ++++++++- .../storage/PhabricatorProjectColumnPosition.php | 11 +++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index e614ec2f94..83fb943b76 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -229,7 +229,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { $this->addQueue[] = $object_position; $positions[$object_phid] = $object_position; - $positions = msort($positions, 'getOrderingKey'); + $positions = msortv($positions, 'newColumnPositionOrderVector'); $this->boardLayout[$board_phid][$column_phid] = $positions; @@ -404,7 +404,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->withBoardPHIDs(array_keys($boards)) ->withObjectPHIDs($object_phids) ->execute(); - $positions = msort($positions, 'getOrderingKey'); + $positions = msortv($positions, 'newColumnPositionOrderVector'); $positions = mgroup($positions, 'getBoardPHID'); return $positions; @@ -581,7 +581,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } foreach ($layout as $column_phid => $map) { - $map = msort($map, 'getOrderingKey'); + $map = msortv($map, 'newColumnPositionOrderVector'); $layout[$column_phid] = $map; foreach ($map as $object_phid => $position) { diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index 96a6d8c457..81e56d2116 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -100,10 +100,17 @@ final class PhabricatorBoardResponseEngine extends Phobject { $all_objects = mpull($all_objects, null, 'getPHID'); } + // NOTE: The board layout engine is sensitive to PHID input order, and uses + // the input order as a component of the "natural" column ordering if no + // explicit ordering is specified. Rearrange the PHIDs in ID order. + + $all_objects = msort($all_objects, 'getID'); + $ordered_phids = mpull($all_objects, 'getPHID'); + $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) - ->setObjectPHIDs($all_phids) + ->setObjectPHIDs($ordered_phids) ->executeLayout(); $natural = array(); diff --git a/src/applications/project/storage/PhabricatorProjectColumnPosition.php b/src/applications/project/storage/PhabricatorProjectColumnPosition.php index 0bd9be6d4a..1a094dec37 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnPosition.php +++ b/src/applications/project/storage/PhabricatorProjectColumnPosition.php @@ -46,7 +46,7 @@ final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO return $this; } - public function getOrderingKey() { + public function newColumnPositionOrderVector() { // We're ordering both real positions and "virtual" positions which we have // created but not saved yet. @@ -61,11 +61,10 @@ final class PhabricatorProjectColumnPosition extends PhabricatorProjectDAO // Broadly, this collectively makes newly added stuff float to the top. - return sprintf( - '~%012d%012d%012d', - $this->getSequence(), - ((1 << 31) - $this->viewSequence), - ((1 << 31) - $this->getID())); + return id(new PhutilSortVector()) + ->addInt($this->getSequence()) + ->addInt(-1 * $this->viewSequence) + ->addInt(-1 * $this->getID()); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ From 0b0ab1bd7cfddf938c4be793c8c481fb55bb8cfa Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 10:06:34 -0700 Subject: [PATCH 51/54] Add a "slowvote.poll.search" API method Summary: Ref T13350. Add a modern "*.search" API method for Slowvote so "slowvote.info" can be deprecated with a reasonable replacement. Test Plan: Used Conduit test console to call method, saw reasonable results. Maniphest Tasks: T13350 Differential Revision: https://secure.phabricator.com/D20685 --- src/__phutil_library_map__.php | 3 ++ .../SlowvoteSearchConduitAPIMethod.php | 18 +++++++++++ .../query/PhabricatorSlowvoteSearchEngine.php | 14 ++++++--- .../storage/PhabricatorSlowvotePoll.php | 31 +++++++++++++++++-- 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0f687ca681..5abfd1bc4d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5614,6 +5614,7 @@ phutil_register_library_map(array( 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteInfoConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', + 'SlowvoteSearchConduitAPIMethod' => 'applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php', 'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php', 'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php', 'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php', @@ -11075,6 +11076,7 @@ phutil_register_library_map(array( 'PhabricatorProjectInterface', 'PhabricatorDestructibleInterface', 'PhabricatorSpacesInterface', + 'PhabricatorConduitResultInterface', ), 'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvotePollPHIDType' => 'PhabricatorPHIDType', @@ -12221,6 +12223,7 @@ phutil_register_library_map(array( 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteInfoConduitAPIMethod' => 'SlowvoteConduitAPIMethod', 'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'SlowvoteSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'SubscriptionListDialogBuilder' => 'Phobject', 'SubscriptionListStringBuilder' => 'Phobject', 'TokenConduitAPIMethod' => 'ConduitAPIMethod', diff --git a/src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php b/src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php new file mode 100644 index 0000000000..01f3255f37 --- /dev/null +++ b/src/applications/slowvote/conduit/SlowvoteSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +setKey('authorPHIDs') ->setAliases(array('authors')) ->setLabel(pht('Authors')), - id(new PhabricatorSearchCheckboxesField()) ->setKey('voted') + ->setLabel(pht('Voted')) + + // TODO: This should probably become a list of "voterPHIDs", so hide + // the field from Conduit to avoid a backward compatibility break when + // this changes. + + ->setEnableForConduit(false) ->setOptions(array( 'voted' => pht("Show only polls I've voted in."), )), - id(new PhabricatorSearchCheckboxesField()) ->setKey('statuses') ->setLabel(pht('Statuses')) - ->setOptions(array( + ->setOptions( + array( 'open' => pht('Open'), 'closed' => pht('Closed'), - )), + )), ); } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index b8355c0586..215549f7db 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -9,7 +9,8 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO PhabricatorTokenReceiverInterface, PhabricatorProjectInterface, PhabricatorDestructibleInterface, - PhabricatorSpacesInterface { + PhabricatorSpacesInterface, + PhabricatorConduitResultInterface { const RESPONSES_VISIBLE = 0; const RESPONSES_VOTERS = 1; @@ -202,10 +203,36 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO $this->saveTransaction(); } - /* -( PhabricatorSpacesInterface )--------------------------------------- */ +/* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the poll.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('authorPHID') + ->setType('string') + ->setDescription(pht('The author of the poll.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getQuestion(), + 'authorPHID' => $this->getAuthorPHID(), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } From f92480fb77409b434cd85012e194010379549b52 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 10:06:34 -0700 Subject: [PATCH 52/54] Fix two minor display issues with the Conduit "*.search" API documentation Summary: Depends on D20685. Ref T13350. Currently: - When a SearchEngine parameter is marked as hidden from Conduit, we may still render a table of possible values. Instead, only render the table if the parameter is actually usable. - The table header is hard-coded to say `'statuses'`, which is just a silly mistake. (Most commonly, this table does have `statuses` constants.) Test Plan: Viewed the Conduit API documentation for the new "slowvote.poll.search" API method, saw more sensible display behavior. Maniphest Tasks: T13350 Differential Revision: https://secure.phabricator.com/D20686 --- .../search/engine/PhabricatorSearchEngineAPIMethod.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php index f4e2dc918f..3af4f349fa 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php +++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php @@ -198,6 +198,7 @@ EOTEXT $label = $field->getLabel(); $constants = $field->newConduitConstants(); + $show_table = false; $type_object = $field->getConduitParameterType(); if ($type_object) { @@ -209,6 +210,7 @@ EOTEXT ' ', phutil_tag('em', array(), pht('(See table below.)')), ); + $show_table = true; } } else { $type = null; @@ -222,11 +224,11 @@ EOTEXT $description, ); - if ($constants) { + if ($show_table) { $constant_lists[] = $this->newRemarkupDocumentationView( pht( 'Constants supported by the `%s` constraint:', - 'statuses')); + $key)); $constants_rows = array(); foreach ($constants as $constant) { From 2ec39afcd12b34e63439304f02af6feaabe87ccc Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 11:24:40 -0700 Subject: [PATCH 53/54] Deprecate ancient "slowvote.info" API method Summary: Depends on D20686. Fixes T13350. Now that "slowvote.poll.search" exists, deprecate this old method. Test Plan: Reviewed method description in Condiut API console in the web UI. Maniphest Tasks: T13350 Differential Revision: https://secure.phabricator.com/D20687 --- .../slowvote/conduit/SlowvoteInfoConduitAPIMethod.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php index 4041b1f70c..1b4cde9191 100644 --- a/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php +++ b/src/applications/slowvote/conduit/SlowvoteInfoConduitAPIMethod.php @@ -6,6 +6,14 @@ final class SlowvoteInfoConduitAPIMethod extends SlowvoteConduitAPIMethod { return 'slowvote.info'; } + public function getMethodStatus() { + return self::METHOD_STATUS_DEPRECATED; + } + + public function getMethodStatusDescription() { + return pht('Replaced by "slowvote.poll.search".'); + } + public function getMethodDescription() { return pht('Retrieve an array of information about a poll.'); } From 47d497aa604580d8b3a6d54e13602f505434394b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 11:37:02 -0700 Subject: [PATCH 54/54] When users visit a Phame post URI with an old blog ID, canonicalize the URI instead of 404'ing Summary: Fixes T13353. If you: - Visit a blog post and save the URI. - Move the blog post to a different blog. - Revisit the old URI. ...we currently 404. We know what you're trying to do and should just redirect you to the new URI instead. We already do this if you visit a URI with a noncanonical slug. Test Plan: - Created post A. - Copied the live URI. - Moved it to a different blog. - Visited the saved URI from the earlier step. - Before: 404. - After: Redirect to the canonical URI. Maniphest Tasks: T13353 Differential Revision: https://secure.phabricator.com/D20688 --- .../phame/controller/PhameLiveController.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/applications/phame/controller/PhameLiveController.php b/src/applications/phame/controller/PhameLiveController.php index b5b1984816..472f73c8c1 100644 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -93,10 +93,6 @@ abstract class PhameLiveController extends PhameController { ->needHeaderImage(true) ->withIDs(array($post_id)); - if ($blog) { - $post_query->withBlogPHIDs(array($blog->getPHID())); - } - // Only show published posts on external domains. if ($is_external) { $post_query->withVisibility( @@ -123,10 +119,15 @@ abstract class PhameLiveController extends PhameController { $this->post = $post; // If we have a post, canonicalize the URI to the post's current slug and - // redirect the user if it isn't correct. + // redirect the user if it isn't correct. Likewise, canonicalize the URI + // if the blog ID is wrong. See T13353. if ($post) { $slug = $request->getURIData('slug'); - if ($post->getSlug() != $slug) { + + $wrong_slug = ($post->getSlug() !== $slug); + $wrong_blog = ($post->getBlog()->getID() !== $blog->getID()); + + if ($wrong_slug || $wrong_blog) { if ($is_live) { if ($is_external) { $uri = $post->getExternalLiveURI();