From 53f8ad14fa929077658445bf4cd82f01f267f636 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 11:10:52 -0700 Subject: [PATCH 001/122] Fix an issue in Owners where a transaction change could show too many effects Summary: Fixes T13324. Ref PHI1288. Currently, if you edit an Owners package that has some paths with no trailing slashes (like `README.md`) so their internal names and display names differ (`/README.md` display, vs `/README.md/` internal), the "Show Details" in the transaction log shows the path as re-normalized even if you didn't touch it. Instead, be more careful about handling display paths vs internal paths. (This code on the whole is significantly less clear than it probably could be, but this issue is so minor that I'm hesitant to start ripping things out.) Test Plan: - In a package with some paths like `/src/` and some paths like `/src`: - Added new paths. - Removed paths. - Changed paths from `/src/` to `/src`. - Changed paths from `/src` to `/src/`. In all cases, the "paths" list and the transaction record identically reflected the edit in the way I expected them to. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13324 Differential Revision: https://secure.phabricator.com/D20596 --- .../owners/storage/PhabricatorOwnersPath.php | 16 +++++++-- ...abricatorOwnersPackagePathsTransaction.php | 36 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/applications/owners/storage/PhabricatorOwnersPath.php b/src/applications/owners/storage/PhabricatorOwnersPath.php index 7022cb887c..5bb32f8407 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPath.php +++ b/src/applications/owners/storage/PhabricatorOwnersPath.php @@ -79,15 +79,27 @@ final class PhabricatorOwnersPath extends PhabricatorOwnersDAO { public static function getSetFromTransactionValue(array $v) { $set = array(); foreach ($v as $ref) { - $set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']] = true; + $key = self::getScalarKeyForRef($ref); + $set[$key] = true; } return $set; } public static function isRefInSet(array $ref, array $set) { - return isset($set[$ref['repositoryPHID']][$ref['path']][$ref['excluded']]); + $key = self::getScalarKeyForRef($ref); + return isset($set[$key]); } + private static function getScalarKeyForRef(array $ref) { + return sprintf( + 'repository=%s path=%s display=%s excluded=%d', + $ref['repositoryPHID'], + $ref['path'], + $ref['display'], + $ref['excluded']); + } + + /** * Get the number of directory matches between this path specification and * some real path. diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php index 753b6ff9e9..a8bb1cc259 100644 --- a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php +++ b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php @@ -12,12 +12,33 @@ final class PhabricatorOwnersPackagePathsTransaction public function generateNewValue($object, $value) { $new = $value; + foreach ($new as $key => $info) { - $new[$key]['excluded'] = (int)idx($info, 'excluded'); + $info['excluded'] = (int)idx($info, 'excluded'); + + // The input has one "path" key with the display path. + // Move it to "display", then normalize the value in "path". + + $display_path = $info['path']; + $raw_path = rtrim($display_path, '/').'/'; + + $info['path'] = $raw_path; + $info['display'] = $display_path; + + $new[$key] = $info; } + return $new; } + public function getTransactionHasEffect($object, $old, $new) { + list($add, $rem) = PhabricatorOwnersPath::getTransactionValueChanges( + $old, + $new); + + return ($add || $rem); + } + public function validateTransactions($object, array $xactions) { $errors = array(); @@ -110,8 +131,8 @@ final class PhabricatorOwnersPackagePathsTransaction $display_map = array(); $seen_map = array(); foreach ($new as $key => $spec) { - $display_path = $spec['path']; - $raw_path = rtrim($display_path, '/').'/'; + $raw_path = $spec['path']; + $display_path = $spec['display']; // If the user entered two paths in the same repository which normalize // to the same value (like "src/main.c" and "src/main.c/"), discard the @@ -193,11 +214,18 @@ final class PhabricatorOwnersPackagePathsTransaction $rowc = array(); foreach ($rows as $key => $row) { $rowc[] = $row['class']; + + if (array_key_exists('display', $row)) { + $display_path = $row['display']; + } else { + $display_path = $row['path']; + } + $rows[$key] = array( $row['change'], $row['excluded'] ? pht('Exclude') : pht('Include'), $this->renderHandle($row['repositoryPHID']), - $row['path'], + $display_path, ); } From c0dc411d231989b01c377eed21f39dd8f0ea4b9f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 12:13:28 -0700 Subject: [PATCH 002/122] Update "phabricator/" for "topological" API changes Summary: Ref T13325. Test Plan: - Grepped for `topograph`. - Viewed a task graph since that's easy, looked fine. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13325 Differential Revision: https://secure.phabricator.com/D20599 --- .../harbormaster/engine/HarbormasterBuildGraph.php | 3 +-- .../engine/PhabricatorRepositoryDiscoveryEngine.php | 4 ++-- src/infrastructure/graph/PhabricatorObjectGraph.php | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php index dc9e4b7691..64c12c2e8f 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php @@ -25,8 +25,7 @@ final class HarbormasterBuildGraph extends AbstractDirectedGraph { $graph = id(new HarbormasterBuildGraph($steps_by_phid)) ->addNodes($step_phids); - $raw_results = - $graph->getBestEffortTopographicallySortedNodes(); + $raw_results = $graph->getNodesInRoughTopologicalOrder(); $results = array(); foreach ($raw_results as $node) { diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index 038fa416f8..c1bb9190b3 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -398,7 +398,7 @@ final class PhabricatorRepositoryDiscoveryEngine } } - // Now, sort them topographically. + // Now, sort them topologically. $commits = $this->reduceGraph($graph); $refs = array(); @@ -437,7 +437,7 @@ final class PhabricatorRepositoryDiscoveryEngine $graph = new PhutilDirectedScalarGraph(); $graph->addNodes($edges); - $commits = $graph->getTopographicallySortedNodes(); + $commits = $graph->getNodesInTopologicalOrder(); // NOTE: We want the most ancestral nodes first, so we need to reverse the // list we get out of AbstractDirectedGraph. diff --git a/src/infrastructure/graph/PhabricatorObjectGraph.php b/src/infrastructure/graph/PhabricatorObjectGraph.php index 48e0fcbe73..67130e8c05 100644 --- a/src/infrastructure/graph/PhabricatorObjectGraph.php +++ b/src/infrastructure/graph/PhabricatorObjectGraph.php @@ -189,7 +189,7 @@ abstract class PhabricatorObjectGraph $order = id(new PhutilDirectedScalarGraph()) ->addNodes($ancestry) - ->getTopographicallySortedNodes(); + ->getNodesInTopologicalOrder(); $ancestry = array_select_keys($ancestry, $order); From 6b9f4a918b857e0f00bae6704474ffe34b232daf Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 19 Jun 2019 15:16:16 -0700 Subject: [PATCH 003/122] Modularize PhabricatorEditEngineConfigurationTransaction Summary: Ref T13319. Ref PHI1302. Migrate `PhabricatorEditEngineConfigurationTransaction` to modular transactions and add some additional transaction rendering to make these edits less opaque. Test Plan: Hit all the form edit controllers, viewed resulting transaction timeline. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T13319 Differential Revision: https://secure.phabricator.com/D20595 --- src/__phutil_library_map__.php | 32 ++- .../xaction/HeraldRuleEditTransaction.php | 13 +- ...neConfigurationDefaultCreateController.php | 2 +- ...tEngineConfigurationDefaultsController.php | 2 +- ...itEngineConfigurationDisableController.php | 2 +- ...ditEngineConfigurationIsEditController.php | 3 +- ...rEditEngineConfigurationLockController.php | 3 +- ...itEngineConfigurationReorderController.php | 3 +- ...rEditEngineConfigurationSortController.php | 4 +- ...itEngineConfigurationSubtypeController.php | 4 +- ...catorEditEngineConfigurationEditEngine.php | 4 +- ...abricatorEditEngineConfigurationEditor.php | 182 --------------- ...atorEditEngineConfigurationTransaction.php | 213 +----------------- ...icatorApplicationTransactionDetailView.php | 172 ++++++++++++++ ...plicationTransactionJSONDiffDetailView.php | 17 ++ ...plicationTransactionTextDiffDetailView.php | 172 +------------- ...icatorEditEngineCreateOrderTransaction.php | 26 +++ ...atorEditEngineDefaultCreateTransaction.php | 35 +++ ...habricatorEditEngineDefaultTransaction.php | 71 ++++++ ...habricatorEditEngineDisableTransaction.php | 51 +++++ ...bricatorEditEngineEditOrderTransaction.php | 26 +++ ...PhabricatorEditEngineIsEditTransaction.php | 34 +++ .../PhabricatorEditEngineLocksTransaction.php | 35 +++ .../PhabricatorEditEngineNameTransaction.php | 54 +++++ .../PhabricatorEditEngineOrderTransaction.php | 35 +++ ...abricatorEditEnginePreambleTransaction.php | 45 ++++ ...habricatorEditEngineSubtypeTransaction.php | 48 ++++ .../PhabricatorEditEngineTransactionType.php | 4 + 28 files changed, 701 insertions(+), 591 deletions(-) create mode 100644 src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php create mode 100644 src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php create mode 100644 src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 424d885caa..ab5a58dda6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2172,9 +2172,11 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionCommentView' => 'applications/transactions/view/PhabricatorApplicationTransactionCommentView.php', 'PhabricatorApplicationTransactionController' => 'applications/transactions/controller/PhabricatorApplicationTransactionController.php', 'PhabricatorApplicationTransactionDetailController' => 'applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php', + 'PhabricatorApplicationTransactionDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionDetailView.php', 'PhabricatorApplicationTransactionEditor' => 'applications/transactions/editor/PhabricatorApplicationTransactionEditor.php', 'PhabricatorApplicationTransactionFeedStory' => 'applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php', 'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php', + 'PhabricatorApplicationTransactionJSONDiffDetailView' => 'applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php', 'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php', 'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php', 'PhabricatorApplicationTransactionPublishWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php', @@ -3151,16 +3153,26 @@ phutil_register_library_map(array( 'PhabricatorEditEngineConfigurationTransactionQuery' => 'applications/transactions/query/PhabricatorEditEngineConfigurationTransactionQuery.php', 'PhabricatorEditEngineConfigurationViewController' => 'applications/transactions/controller/PhabricatorEditEngineConfigurationViewController.php', 'PhabricatorEditEngineController' => 'applications/transactions/controller/PhabricatorEditEngineController.php', + 'PhabricatorEditEngineCreateOrderTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php', 'PhabricatorEditEngineDatasource' => 'applications/transactions/typeahead/PhabricatorEditEngineDatasource.php', + 'PhabricatorEditEngineDefaultCreateTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php', 'PhabricatorEditEngineDefaultLock' => 'applications/transactions/editengine/PhabricatorEditEngineDefaultLock.php', + 'PhabricatorEditEngineDefaultTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php', + 'PhabricatorEditEngineDisableTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php', + 'PhabricatorEditEngineEditOrderTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php', 'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php', 'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php', + 'PhabricatorEditEngineIsEditTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php', 'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php', 'PhabricatorEditEngineLock' => 'applications/transactions/editengine/PhabricatorEditEngineLock.php', 'PhabricatorEditEngineLockableInterface' => 'applications/transactions/editengine/PhabricatorEditEngineLockableInterface.php', + 'PhabricatorEditEngineLocksTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php', 'PhabricatorEditEngineMFAEngine' => 'applications/transactions/editengine/PhabricatorEditEngineMFAEngine.php', 'PhabricatorEditEngineMFAInterface' => 'applications/transactions/editengine/PhabricatorEditEngineMFAInterface.php', + 'PhabricatorEditEngineNameTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php', + 'PhabricatorEditEngineOrderTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php', 'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php', + 'PhabricatorEditEnginePreambleTransaction' => 'applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php', 'PhabricatorEditEngineProfileMenuItem' => 'applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php', 'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php', 'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php', @@ -3171,7 +3183,9 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', 'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php', 'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', + 'PhabricatorEditEngineSubtypeTransaction' => 'applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php', 'PhabricatorEditEngineTokenizerCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineTokenizerCommentAction.php', + 'PhabricatorEditEngineTransactionType' => 'applications/transactions/xaction/PhabricatorEditEngineTransactionType.php', 'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php', 'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php', 'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php', @@ -8091,8 +8105,10 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionCommentView' => 'AphrontView', 'PhabricatorApplicationTransactionController' => 'PhabricatorController', 'PhabricatorApplicationTransactionDetailController' => 'PhabricatorApplicationTransactionController', + 'PhabricatorApplicationTransactionDetailView' => 'AphrontView', 'PhabricatorApplicationTransactionEditor' => 'PhabricatorEditor', 'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory', + 'PhabricatorApplicationTransactionJSONDiffDetailView' => 'PhabricatorApplicationTransactionDetailView', 'PhabricatorApplicationTransactionNoEffectException' => 'Exception', 'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionPublishWorker' => 'PhabricatorWorker', @@ -8103,7 +8119,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionStructureException' => 'Exception', 'PhabricatorApplicationTransactionTemplatedCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', - 'PhabricatorApplicationTransactionTextDiffDetailView' => 'AphrontView', + 'PhabricatorApplicationTransactionTextDiffDetailView' => 'PhabricatorApplicationTransactionDetailView', 'PhabricatorApplicationTransactionTransactionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorApplicationTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorApplicationTransactionValidationError' => 'Phobject', @@ -9228,18 +9244,28 @@ phutil_register_library_map(array( 'PhabricatorEditEngineConfigurationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorEditEngineConfigurationSortController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineConfigurationSubtypeController' => 'PhabricatorEditEngineController', - 'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorEditEngineConfigurationTransaction' => 'PhabricatorModularTransaction', 'PhabricatorEditEngineConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorEditEngineConfigurationViewController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineController' => 'PhabricatorApplicationTransactionController', + 'PhabricatorEditEngineCreateOrderTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorEditEngineDefaultCreateTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineDefaultLock' => 'PhabricatorEditEngineLock', + 'PhabricatorEditEngineDefaultTransaction' => 'PhabricatorEditEngineTransactionType', + 'PhabricatorEditEngineDisableTransaction' => 'PhabricatorEditEngineTransactionType', + 'PhabricatorEditEngineEditOrderTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineExtension' => 'Phobject', 'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule', + 'PhabricatorEditEngineIsEditTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController', 'PhabricatorEditEngineLock' => 'Phobject', + 'PhabricatorEditEngineLocksTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineMFAEngine' => 'Phobject', + 'PhabricatorEditEngineNameTransaction' => 'PhabricatorEditEngineTransactionType', + 'PhabricatorEditEngineOrderTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction', + 'PhabricatorEditEnginePreambleTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -9249,7 +9275,9 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSubtype' => 'Phobject', 'PhabricatorEditEngineSubtypeMap' => 'Phobject', 'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', + 'PhabricatorEditEngineSubtypeTransaction' => 'PhabricatorEditEngineTransactionType', 'PhabricatorEditEngineTokenizerCommentAction' => 'PhabricatorEditEngineCommentAction', + 'PhabricatorEditEngineTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorEditField' => 'Phobject', 'PhabricatorEditPage' => 'Phobject', 'PhabricatorEditType' => 'Phobject', diff --git a/src/applications/herald/xaction/HeraldRuleEditTransaction.php b/src/applications/herald/xaction/HeraldRuleEditTransaction.php index c4b03983fb..653d300906 100644 --- a/src/applications/herald/xaction/HeraldRuleEditTransaction.php +++ b/src/applications/herald/xaction/HeraldRuleEditTransaction.php @@ -40,17 +40,10 @@ final class HeraldRuleEditTransaction public function newChangeDetailView() { $viewer = $this->getViewer(); - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $json = new PhutilJSON(); - $old_json = $json->encodeFormatted($old); - $new_json = $json->encodeFormatted($new); - - return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + return id(new PhabricatorApplicationTransactionJSONDiffDetailView()) ->setViewer($viewer) - ->setOldText($old_json) - ->setNewText($new_json); + ->setOld($this->getOldValue()) + ->setNew($this->getNewValue()); } } diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php index d7ea8810a5..039bfb0f44 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultCreateController.php @@ -15,7 +15,7 @@ final class PhabricatorEditEngineConfigurationDefaultCreateController $key = $config->getIdentifier(); $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE; + $type = PhabricatorEditEngineDefaultCreateTransaction::TRANSACTIONTYPE; if ($request->isFormPost()) { $xactions = array(); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php index f7361d50cf..3d63a4a098 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDefaultsController.php @@ -52,7 +52,7 @@ final class PhabricatorEditEngineConfigurationDefaultsController $field->readValueFromSubmit($request); } - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT; + $type = PhabricatorEditEngineDefaultTransaction::TRANSACTIONTYPE; $xactions = array(); foreach ($fields as $field) { diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php index 24a32c5598..a3311b2d49 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationDisableController.php @@ -15,7 +15,7 @@ final class PhabricatorEditEngineConfigurationDisableController $key = $config->getIdentifier(); $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE; + $type = PhabricatorEditEngineDisableTransaction::TRANSACTIONTYPE; if ($request->isFormPost()) { $xactions = array(); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php index 970a2512f1..b93390ff66 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationIsEditController.php @@ -15,8 +15,7 @@ final class PhabricatorEditEngineConfigurationIsEditController $key = $config->getIdentifier(); $cancel_uri = "/transactions/editengine/{$engine_key}/view/{$key}/"; - $type = PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT; - + $type = PhabricatorEditEngineIsEditTransaction::TRANSACTIONTYPE; if ($request->isFormPost()) { $xactions = array(); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php index 34b099b9f0..1375124585 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationLockController.php @@ -30,8 +30,7 @@ final class PhabricatorEditEngineConfigurationLockController $xactions = array(); $locks = $request->getArr('locks'); - $type_locks = PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS; - + $type_locks = PhabricatorEditEngineLocksTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_locks) ->setNewValue($locks); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php index 6ff36cdfa4..563c3141b2 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationReorderController.php @@ -31,8 +31,7 @@ final class PhabricatorEditEngineConfigurationReorderController $xactions = array(); $key_order = $request->getStrList('keyOrder'); - $type_order = PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; - + $type_order = PhabricatorEditEngineOrderTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($key_order); diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php index 613a847326..5e8680b651 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSortController.php @@ -70,10 +70,10 @@ final class PhabricatorEditEngineConfigurationSortController if ($is_create) { $xaction_type = - PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER; + PhabricatorEditEngineCreateOrderTransaction::TRANSACTIONTYPE; } else { $xaction_type = - PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER; + PhabricatorEditEngineEditOrderTransaction::TRANSACTIONTYPE; } $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) diff --git a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php index 9ce8d7a0e5..918782cd81 100644 --- a/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php +++ b/src/applications/transactions/controller/PhabricatorEditEngineConfigurationSubtypeController.php @@ -35,9 +35,7 @@ final class PhabricatorEditEngineConfigurationSubtypeController $xactions = array(); $subtype = $request->getStr('subtype'); - $type_subtype = - PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE; - + $type_subtype = PhabricatorEditEngineSubtypeTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorEditEngineConfigurationTransaction()) ->setTransactionType($type_subtype) ->setNewValue($subtype); diff --git a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php index a9d96337ed..5a30fbfdde 100644 --- a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php +++ b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditEngine.php @@ -99,14 +99,14 @@ final class PhabricatorEditEngineConfigurationEditEngine ->setLabel(pht('Name')) ->setDescription(pht('Name of the form.')) ->setTransactionType( - PhabricatorEditEngineConfigurationTransaction::TYPE_NAME) + PhabricatorEditEngineNameTransaction::TRANSACTIONTYPE) ->setValue($object->getName()), id(new PhabricatorRemarkupEditField()) ->setKey('preamble') ->setLabel(pht('Preamble')) ->setDescription(pht('Optional instructions, shown above the form.')) ->setTransactionType( - PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE) + PhabricatorEditEnginePreambleTransaction::TRANSACTIONTYPE) ->setValue($object->getPreamble()), ); } diff --git a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php index ccadf9b819..34b7653001 100644 --- a/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php +++ b/src/applications/transactions/editor/PhabricatorEditEngineConfigurationEditor.php @@ -13,191 +13,9 @@ final class PhabricatorEditEngineConfigurationEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_NAME; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE; - $types[] = - PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE; - - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER; - $types[] = PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER; - return $types; } - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - switch ($type) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Form name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - if ($xactions) { - $map = $object->getEngine() - ->setViewer($this->getActor()) - ->newSubtypeMap(); - foreach ($xactions as $xaction) { - $new = $xaction->getNewValue(); - - if ($map->isValidSubtype($new)) { - continue; - } - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('Subtype "%s" is not a valid subtype.', $new), - $xaction); - } - } - break; - } - - return $errors; - } - - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - return $object->getName(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - return $object->getPreamble(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER: - return $object->getFieldOrder(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - $field_key = $xaction->getMetadataValue('field.key'); - return $object->getFieldDefault($field_key); - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - return $object->getFieldLocks(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - return $object->getSubtype(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - return (int)$object->getIsDefault(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - return (int)$object->getIsEdit(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - return (int)$object->getIsDisabled(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - return (int)$object->getCreateOrder(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - return (int)$object->getEditOrder(); - - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - return $xaction->getNewValue(); - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - return (int)$xaction->getNewValue(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - $object->setPreamble($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER: - $object->setFieldOrder($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - $field_key = $xaction->getMetadataValue('field.key'); - $object->setFieldDefault($field_key, $xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - $object->setFieldLocks($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - $object->setSubtype($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - $object->setIsDefault($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - $object->setIsEdit($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - $object->setIsDisabled($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - $object->setCreateOrder($xaction->getNewValue()); - return; - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - $object->setEditOrder($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorEditEngineConfigurationTransaction::TYPE_NAME: - case PhabricatorEditEngineConfigurationTransaction::TYPE_PREAMBLE; - case PhabricatorEditEngineConfigurationTransaction::TYPE_ORDER; - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_ISEDIT: - case PhabricatorEditEngineConfigurationTransaction::TYPE_LOCKS: - case PhabricatorEditEngineConfigurationTransaction::TYPE_SUBTYPE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DEFAULTCREATE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_DISABLE: - case PhabricatorEditEngineConfigurationTransaction::TYPE_CREATEORDER: - case PhabricatorEditEngineConfigurationTransaction::TYPE_EDITORDER: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - } diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php index bf23bd3b4a..a9dca32e3d 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php @@ -1,19 +1,7 @@ getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return pht( - '%s created this form configuration.', - $this->renderHandleLink($author_phid)); - case self::TYPE_NAME: - if (strlen($old)) { - return pht( - '%s renamed this form from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } else { - return pht( - '%s named this form "%s".', - $this->renderHandleLink($author_phid), - $new); - } - case self::TYPE_PREAMBLE: - return pht( - '%s updated the preamble for this form.', - $this->renderHandleLink($author_phid)); - case self::TYPE_ORDER: - return pht( - '%s reordered the fields in this form.', - $this->renderHandleLink($author_phid)); - case self::TYPE_DEFAULT: - $key = $this->getMetadataValue('field.key'); - - $object = $this->getObject(); - $engine = $object->getEngine(); - $fields = $engine->getFieldsForConfig($object); - - $field = idx($fields, $key); - if (!$field) { - return pht( - '%s changed the default value for field "%s".', - $this->renderHandleLink($author_phid), - $key); - } - - return pht( - '%s changed the default value for field "%s".', - $this->renderHandleLink($author_phid), - $field->getLabel()); - case self::TYPE_LOCKS: - return pht( - '%s changed locked and hidden fields.', - $this->renderHandleLink($author_phid)); - case self::TYPE_DEFAULTCREATE: - if ($new) { - return pht( - '%s added this form to the "Create" menu.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s removed this form from the "Create" menu.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_ISEDIT: - if ($new) { - return pht( - '%s marked this form as an edit form.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s unmarked this form as an edit form.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_DISABLE: - if ($new) { - return pht( - '%s disabled this form.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled this form.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_SUBTYPE: - return pht( - '%s changed the subtype of this form from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - return parent::getTitle(); - } - - public function getColor() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return 'green'; - case self::TYPE_DISABLE: - if ($new) { - return 'indigo'; - } else { - return 'green'; - } - } - - return parent::getColor(); - } - - public function getIcon() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $type = $this->getTransactionType(); - switch ($type) { - case PhabricatorTransactions::TYPE_CREATE: - return 'fa-plus'; - case self::TYPE_DISABLE: - if ($new) { - return 'fa-ban'; - } else { - return 'fa-check'; - } - } - - return parent::getIcon(); - } - - protected function newRemarkupChanges() { - $changes = array(); - - $type = $this->getTransactionType(); - switch ($type) { - case self::TYPE_PREAMBLE: - $changes[] = $this->newRemarkupChange() - ->setOldValue($this->getOldValue()) - ->setNewValue($this->getNewValue()); - break; - } - - return $changes; - } - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_DEFAULT: - return true; - } - - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - switch ($this->getTransactionType()) { - case self::TYPE_DEFAULT: - $old_value = $this->getOldValue(); - $new_value = $this->getNewValue(); - - $old_value = $this->renderDefaultValueAsFallbackText($old_value); - $new_value = $this->renderDefaultValueAsFallbackText($new_value); - - return $this->renderTextCorpusChangeDetails( - $viewer, - $old_value, - $new_value); - } - - return parent::renderChangeDetails($viewer); - } - - private function renderDefaultValueAsFallbackText($default_value) { - // See T13319. When rendering an "alice changed the default value for - // field X." story on custom forms, we may fall back to a generic - // rendering. Try to present the value change in a comprehensible way - // even if it isn't especially human readable (for example, it may - // contain PHIDs or other internal identifiers). - - if (is_scalar($default_value) || is_null($default_value)) { - return $default_value; - } - - if (phutil_is_natural_list($default_value)) { - return id(new PhutilJSON())->encodeAsList($default_value); - } - - return id(new PhutilJSON())->encodeAsObject($default_value); + public function getBaseTransactionClass() { + return 'PhabricatorEditEngineTransactionType'; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php new file mode 100644 index 0000000000..1220e5bc7f --- /dev/null +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionDetailView.php @@ -0,0 +1,172 @@ +newText = $new_text; + return $this; + } + + public function setOldText($old_text) { + $this->oldText = $old_text; + return $this; + } + + public function renderForMail() { + $diff = $this->buildDiff(); + + $viewer = $this->getViewer(); + $old_bright = $viewer->getCSSValue('old-bright'); + $new_bright = $viewer->getCSSValue('new-bright'); + + $old_styles = array( + 'padding: 0 2px;', + 'color: #333333;', + "background: {$old_bright};", + ); + $old_styles = implode(' ', $old_styles); + + $new_styles = array( + 'padding: 0 2px;', + 'color: #333333;', + "background: {$new_bright};", + ); + $new_styles = implode(' ', $new_styles); + + $omit_styles = array( + 'padding: 8px 0;', + ); + $omit_styles = implode(' ', $omit_styles); + + $result = array(); + foreach ($diff->getSummaryParts() as $part) { + $type = $part['type']; + $text = $part['text']; + switch ($type) { + case '.': + $result[] = phutil_tag( + 'div', + array( + 'style' => $omit_styles, + ), + pht('...')); + break; + case '-': + $result[] = phutil_tag( + 'span', + array( + 'style' => $old_styles, + ), + $text); + break; + case '+': + $result[] = phutil_tag( + 'span', + array( + 'style' => $new_styles, + ), + $text); + break; + case '=': + $result[] = $text; + break; + } + } + + $styles = array( + 'white-space: pre-wrap;', + 'color: #74777D;', + ); + + // Beyond applying "pre-wrap", convert newlines to "
" explicitly + // to improve behavior in clients like Airmail. + $result = phutil_escape_html_newlines($result); + + return phutil_tag( + 'div', + array( + 'style' => implode(' ', $styles), + ), + $result); + } + + public function render() { + $diff = $this->buildDiff(); + + require_celerity_resource('differential-changeset-view-css'); + + $result = array(); + foreach ($diff->getParts() as $part) { + $type = $part['type']; + $text = $part['text']; + switch ($type) { + case '-': + $result[] = phutil_tag( + 'span', + array( + 'class' => 'old', + ), + $text); + break; + case '+': + $result[] = phutil_tag( + 'span', + array( + 'class' => 'new', + ), + $text); + break; + case '=': + $result[] = $text; + break; + } + } + + $diff_view = phutil_tag( + 'div', + array( + 'class' => 'prose-diff', + ), + $result); + + $old_view = phutil_tag( + 'div', + array( + 'class' => 'prose-diff', + ), + $this->oldText); + + $new_view = phutil_tag( + 'div', + array( + 'class' => 'prose-diff', + ), + $this->newText); + + return id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setKey('old') + ->setName(pht('Old')) + ->appendChild($old_view)) + ->addTab( + id(new PHUITabView()) + ->setKey('new') + ->setName(pht('New')) + ->appendChild($new_view)) + ->addTab( + id(new PHUITabView()) + ->setKey('diff') + ->setName(pht('Diff')) + ->appendChild($diff_view)) + ->selectTab('diff'); + } + + private function buildDiff() { + $engine = new PhutilProseDifferenceEngine(); + return $engine->getDiff($this->oldText, $this->newText); + } +} diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php new file mode 100644 index 0000000000..91111c1e0b --- /dev/null +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionJSONDiffDetailView.php @@ -0,0 +1,17 @@ +setNewText($json->encodeFormatted($new_object)); + return $this; + } + + public function setOld($old_object) { + $json = new PhutilJSON(); + $this->setOldText($json->encodeFormatted($old_object)); + return $this; + } +} diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php index 755d6a9fcf..d778d8e95d 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php @@ -1,174 +1,4 @@ newText = $new_text; - return $this; - } - - public function setOldText($old_text) { - $this->oldText = $old_text; - return $this; - } - - public function renderForMail() { - $diff = $this->buildDiff(); - - $viewer = $this->getViewer(); - $old_bright = $viewer->getCSSValue('old-bright'); - $new_bright = $viewer->getCSSValue('new-bright'); - - $old_styles = array( - 'padding: 0 2px;', - 'color: #333333;', - "background: {$old_bright};", - ); - $old_styles = implode(' ', $old_styles); - - $new_styles = array( - 'padding: 0 2px;', - 'color: #333333;', - "background: {$new_bright};", - ); - $new_styles = implode(' ', $new_styles); - - $omit_styles = array( - 'padding: 8px 0;', - ); - $omit_styles = implode(' ', $omit_styles); - - $result = array(); - foreach ($diff->getSummaryParts() as $part) { - $type = $part['type']; - $text = $part['text']; - switch ($type) { - case '.': - $result[] = phutil_tag( - 'div', - array( - 'style' => $omit_styles, - ), - pht('...')); - break; - case '-': - $result[] = phutil_tag( - 'span', - array( - 'style' => $old_styles, - ), - $text); - break; - case '+': - $result[] = phutil_tag( - 'span', - array( - 'style' => $new_styles, - ), - $text); - break; - case '=': - $result[] = $text; - break; - } - } - - $styles = array( - 'white-space: pre-wrap;', - 'color: #74777D;', - ); - - // Beyond applying "pre-wrap", convert newlines to "
" explicitly - // to improve behavior in clients like Airmail. - $result = phutil_escape_html_newlines($result); - - return phutil_tag( - 'div', - array( - 'style' => implode(' ', $styles), - ), - $result); - } - - public function render() { - $diff = $this->buildDiff(); - - require_celerity_resource('differential-changeset-view-css'); - - $result = array(); - foreach ($diff->getParts() as $part) { - $type = $part['type']; - $text = $part['text']; - switch ($type) { - case '-': - $result[] = phutil_tag( - 'span', - array( - 'class' => 'old', - ), - $text); - break; - case '+': - $result[] = phutil_tag( - 'span', - array( - 'class' => 'new', - ), - $text); - break; - case '=': - $result[] = $text; - break; - } - } - - $diff_view = phutil_tag( - 'div', - array( - 'class' => 'prose-diff', - ), - $result); - - $old_view = phutil_tag( - 'div', - array( - 'class' => 'prose-diff', - ), - $this->oldText); - - $new_view = phutil_tag( - 'div', - array( - 'class' => 'prose-diff', - ), - $this->newText); - - return id(new PHUITabGroupView()) - ->addTab( - id(new PHUITabView()) - ->setKey('old') - ->setName(pht('Old')) - ->appendChild($old_view)) - ->addTab( - id(new PHUITabView()) - ->setKey('new') - ->setName(pht('New')) - ->appendChild($new_view)) - ->addTab( - id(new PHUITabView()) - ->setKey('diff') - ->setName(pht('Diff')) - ->appendChild($diff_view)) - ->selectTab('diff'); - } - - private function buildDiff() { - $engine = new PhutilProseDifferenceEngine(); - return $engine->getDiff($this->oldText, $this->newText); - } - -} + extends PhabricatorApplicationTransactionDetailView {} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php new file mode 100644 index 0000000000..9a9483c227 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineCreateOrderTransaction.php @@ -0,0 +1,26 @@ +getCreateOrder(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setCreateOrder($value); + } + + public function getTitle() { + return pht( + '%s changed the order in which this form appears in the "Create" menu.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php new file mode 100644 index 0000000000..2c4a544013 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultCreateTransaction.php @@ -0,0 +1,35 @@ +getIsDefault(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsDefault($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s added this form to the "Create" menu.', + $this->renderAuthor()); + } else { + return pht( + '%s removed this form from the "Create" menu.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php new file mode 100644 index 0000000000..7b980cdb1a --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineDefaultTransaction.php @@ -0,0 +1,71 @@ +getMetadataValue('field.key'); + return $object->getFieldDefault($field_key); + } + + public function applyInternalEffects($object, $value) { + $field_key = $this->getMetadataValue('field.key'); + $object->setFieldDefault($field_key, $value); + } + + public function getTitle() { + $key = $this->getMetadataValue('field.key'); + $object = $this->getObject(); + $engine = $object->getEngine(); + $fields = $engine->getFieldsForConfig($object); + $field = idx($fields, $key); + + if (!$field) { + return pht( + '%s changed the default values for field %s.', + $this->renderAuthor(), + $this->renderValue($key)); + } + + return pht( + '%s changed the default value for field %s.', + $this->renderAuthor(), + $this->renderValue($field->getLabel())); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + $old = $this->renderDefaultValueAsFallbackText($this->getOldValue()); + $new = $this->renderDefaultValueAsFallbackText($this->getNewValue()); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old) + ->setNewText($new); + } + + private function renderDefaultValueAsFallbackText($default_value) { + // See T13319. When rendering an "alice changed the default value for + // field X." story on custom forms, we may fall back to a generic + // rendering. Try to present the value change in a comprehensible way + // even if it isn't especially human readable (for example, it may + // contain PHIDs or other internal identifiers). + + if (is_scalar($default_value) || is_null($default_value)) { + return $default_value; + } + + if (phutil_is_natural_list($default_value)) { + return id(new PhutilJSON())->encodeAsList($default_value); + } + + return id(new PhutilJSON())->encodeAsObject($default_value); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php new file mode 100644 index 0000000000..ef64c20974 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineDisableTransaction.php @@ -0,0 +1,51 @@ +getIsDisabled(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new) { + return pht( + '%s disabled this form.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this form.', + $this->renderAuthor()); + } + } + + public function getColor() { + $new = $this->getNewValue(); + if ($new) { + return 'indigo'; + } else { + return 'green'; + } + } + + public function getIcon() { + $new = $this->getNewValue(); + if ($new) { + return 'fa-ban'; + } else { + return 'fa-check'; + } + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php new file mode 100644 index 0000000000..a9109e6475 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineEditOrderTransaction.php @@ -0,0 +1,26 @@ +getEditOrder(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setEditOrder($value); + } + + public function getTitle() { + return pht( + '%s changed the order in which this form appears in the "Edit" menu.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php new file mode 100644 index 0000000000..a4a3a38543 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineIsEditTransaction.php @@ -0,0 +1,34 @@ +getIsEdit(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsEdit($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + if ($new) { + return pht( + '%s marked this form as an edit form.', + $this->renderAuthor()); + } else { + return pht( + '%s unmarked this form as an edit form.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php new file mode 100644 index 0000000000..b919a4de08 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineLocksTransaction.php @@ -0,0 +1,35 @@ +getFieldLocks(); + } + + public function applyInternalEffects($object, $value) { + $object->setFieldLocks($value); + } + + public function getTitle() { + return pht( + '%s changed locked and hidden fields.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionJSONDiffDetailView()) + ->setViewer($viewer) + ->setOld($this->getOldValue()) + ->setNew($this->getNewValue()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php new file mode 100644 index 0000000000..0ab4b7029f --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineNameTransaction.php @@ -0,0 +1,54 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + if (strlen($this->getOldValue())) { + return pht( + '%s renamed this form from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else { + return pht( + '%s named this form %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (!strlen($new)) { + $errors[] = $this->newRequiredError( + pht('Form name is required.'), + $xaction); + continue; + } + } + + if (!$errors) { + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Forms must have a name.')); + } + } + + return $errors; + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php new file mode 100644 index 0000000000..96376113c4 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineOrderTransaction.php @@ -0,0 +1,35 @@ +getFieldOrder(); + } + + public function applyInternalEffects($object, $value) { + $object->setFieldOrder($value); + } + + public function getTitle() { + return pht( + '%s reordered the fields in this form.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionJSONDiffDetailView()) + ->setViewer($viewer) + ->setOld($this->getOldValue()) + ->setNew($this->getNewValue()); + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php new file mode 100644 index 0000000000..e450bc592d --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEnginePreambleTransaction.php @@ -0,0 +1,45 @@ +getPreamble(); + } + + public function applyInternalEffects($object, $value) { + $object->setPreamble($value); + } + + public function getTitle() { + return pht( + '%s updated the preamble for this form.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($this->getOldValue()) + ->setNewText($this->getNewValue()); + } + + public function newRemarkupChanges() { + $changes = array(); + + $changes[] = $this->newRemarkupChange() + ->setOldValue($this->getOldValue()) + ->setNewValue($this->getNewValue()); + + return $changes; + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php new file mode 100644 index 0000000000..53ec221631 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php @@ -0,0 +1,48 @@ +getSubtype(); + } + + public function applyInternalEffects($object, $value) { + $object->setSubtype($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + return pht( + '%s changed the subtype of this form from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $map = $object->getEngine() + ->setViewer($this->getActor()) + ->newSubtypeMap(); + + $errors = array(); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if ($map->isValidSubtype($new)) { + continue; + } + + $errors[] = $this->newInvalidError( + pht('Subtype "%s" is not a valid subtype.', $new), + $xaction); + } + + return $errors; + } + +} diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php b/src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php new file mode 100644 index 0000000000..c49d1e4654 --- /dev/null +++ b/src/applications/transactions/xaction/PhabricatorEditEngineTransactionType.php @@ -0,0 +1,4 @@ + Date: Wed, 19 Jun 2019 10:37:53 -0700 Subject: [PATCH 004/122] Don't handle JIRA/Asana URIs with anchors or query parameters in a special way (with Doorkeeper) Summary: Ref T13291. See PHI1312. Currently, if you link to a JIRA or Asana issue with an anchor (`#asdf`) or query parameters (`?a=b`), we: - treat the link as an external object reference and attempt a lookup on it; - if the lookup succeeds, we discard the fragment or parameters when re-rendering the rich link (with the issue/task title). Particularly, the re-rendering part uses the canonical URI of the object, and can discard these parameters/fragments, which is broken/bad. As a first pass at improving this, just don't apply special behavior for links with anchors or parameters -- simply treat them as links. In some future change, we could specialize this behavior and permit certain known parameters or anchors or something, but these use cases are likely fairly marginal. Test Plan: Before: {F6516392} After: {F6516393} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291 Differential Revision: https://secure.phabricator.com/D20592 --- .../auth/provider/PhabricatorAsanaAuthProvider.php | 8 ++++++++ .../auth/provider/PhabricatorJIRAAuthProvider.php | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php b/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php index 8a0fccec28..4067e522af 100644 --- a/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php @@ -59,6 +59,14 @@ final class PhabricatorAsanaAuthProvider return null; } + if (strlen($uri->getFragment())) { + return null; + } + + if ($uri->getQueryParamsAsPairList()) { + return null; + } + $context_id = $matches[1]; $task_id = $matches[2]; diff --git a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php index 23f7e7f706..c26c70ce37 100644 --- a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php @@ -341,6 +341,14 @@ final class PhabricatorJIRAAuthProvider return null; } + if (strlen($uri->getFragment())) { + return null; + } + + if ($uri->getQueryParamsAsPairList()) { + return null; + } + $domain = $matches[1]; $issue = $matches[2]; From 75c359835903b8d083c8e2c783469c1dfdfa11ea Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 24 Jun 2019 09:23:25 -0700 Subject: [PATCH 005/122] Require commit identities when editing commits to resolve an issue with audit actions not applying properly Summary: See . In D20581, I made some audit behavior dependent upon identities, but the actual edit flow doesn't load them. This can cause us to raise an "attach identities first" exception in the bowels of the edit workflow and trigger unexpected behavior at top level. Load identities when editing a commit so that the transaction flows have access to identity information and can use it to figure out if a user is an author, etc. Test Plan: - As an auditor, applied an "Accept Commit" action to an open audit after D20581. - Before patch: accept no-ops internally since the preconditions throw. - After patch: accept works properly. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20612 --- .../diffusion/editor/DiffusionCommitEditEngine.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php index 5b7277000f..6e45241d68 100644 --- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php @@ -48,7 +48,8 @@ final class DiffusionCommitEditEngine return id(new DiffusionCommitQuery()) ->needCommitData(true) ->needAuditRequests(true) - ->needAuditAuthority(array($viewer)); + ->needAuditAuthority(array($viewer)) + ->needIdentities(true); } protected function getEditorURI() { From d98bf8ef8ee68e34ba2d74173a7be00f3153567f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 13:54:13 -0700 Subject: [PATCH 006/122] Drive "phd stop" entirely from the process list, not PID files on disk Summary: Ref T13321. Depends on D20600. Make `bin/phd stop` mean: - `bin/phd stop`: Stop all processes which have daemon process titles. If we're instanced, only stop daemons for the current instance. - `bin/phd stop --force`: Stop all processes which have deamon process titles for any instance. We no longer read or care about PID files on disk, and this moves us away from PID files. This makes unusual flag `--gently` do nothing. A followup will update the documentation and flags to reflect actual usage/behavior. This also removes the ability to stop specific PIDs. This was somewhat useful long, long ago when you might explicitly run different copies of the `PullLocal` daemon with flags to control which repositories they updated, but with the advent of clustering it's no longer valid to run custom daemon loadouts. Test Plan: Ran `bin/phd start`, then `bin/phd stop`. Saw instance daemons stop. Ran `bin/phd stop --force`, saw all daemons stop. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13321 Differential Revision: https://secure.phabricator.com/D20601 --- ...ricatorDaemonManagementRestartWorkflow.php | 1 - ...habricatorDaemonManagementStopWorkflow.php | 1 - .../PhabricatorDaemonManagementWorkflow.php | 172 ++++++++---------- 3 files changed, 72 insertions(+), 102 deletions(-) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php index 3e9f0af8c3..5a1b46ef1a 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php @@ -35,7 +35,6 @@ final class PhabricatorDaemonManagementRestartWorkflow public function execute(PhutilArgumentParser $args) { $err = $this->executeStopCommand( - array(), array( 'graceful' => $args->getArg('graceful'), 'force' => $args->getArg('force'), diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php index c54a7e9fee..6f982f3e42 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php @@ -42,7 +42,6 @@ final class PhabricatorDaemonManagementStopWorkflow public function execute(PhutilArgumentParser $args) { return $this->executeStopCommand( - $args->getArg('pids'), array( 'graceful' => $args->getArg('graceful'), 'force' => $args->getArg('force'), diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index d5b4ed23e5..e09376195b 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -386,78 +386,86 @@ abstract class PhabricatorDaemonManagementWorkflow return 0; } - final protected function executeStopCommand( - array $pids, - array $options) { - - $console = PhutilConsole::getConsole(); - + final protected function executeStopCommand(array $options) { $grace_period = idx($options, 'graceful', 15); $force = idx($options, 'force'); - $gently = idx($options, 'gently'); - if ($gently && $force) { - throw new PhutilArgumentUsageException( + $query = id(new PhutilProcessQuery()) + ->withIsOverseer(true); + + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + if ($instance !== null && !$force) { + $query->withInstances(array($instance)); + } + + try { + $process_refs = $query->execute(); + } catch (Exception $ex) { + // See T13321. If this fails for some reason, just continue for now so + // that daemon management still works. In the long run, we don't expect + // this to fail, but I don't want to break this workflow while we iron + // bugs out. + + // See T12827. Particularly, this is likely to fail on Solaris. + + phlog($ex); + + $process_refs = array(); + } + + if (!$process_refs) { + if ($instance !== null && !$force) { + $this->logInfo( + pht('NO DAEMONS'), + pht( + 'There are no running daemons for the current instance ("%s"). '. + 'Use "--force" to stop daemons for all instances.', + $instance)); + } else { + $this->logInfo( + pht('NO DAEMONS'), + pht('There are no running daemons.')); + } + + return 0; + } + + $process_refs = mpull($process_refs, null, 'getPID'); + + $stop_pids = array_keys($process_refs); + $live_pids = $this->sendStopSignals($stop_pids, $grace_period); + + $stop_pids = array_fuse($stop_pids); + $live_pids = array_fuse($live_pids); + + $dead_pids = array_diff_key($stop_pids, $live_pids); + + foreach ($dead_pids as $dead_pid) { + $dead_ref = $process_refs[$dead_pid]; + $this->logOkay( + pht('STOP'), pht( - 'You can not specify conflicting options %s and %s together.', - '--gently', - '--force')); + 'Stopped PID %d ("%s")', + $dead_pid, + $dead_ref->getCommand())); } - $daemons = $this->loadRunningDaemons(); - if (!$daemons) { - $survivors = array(); - if (!$pids && !$gently) { - $survivors = $this->processRogueDaemons( - $grace_period, - $warn = true, - $force); - } - if (!$survivors) { - $console->writeErr( - "%s\n", - pht('There are no running Phabricator daemons.')); - } - return 0; + foreach ($live_pids as $live_pid) { + $live_ref = $process_refs[$live_pid]; + $this->logFail( + pht('SURVIVED'), + pht( + 'Unable to stop PID %d ("%s").', + $live_pid, + $live_ref->getCommand())); } - $stop_pids = $this->selectDaemonPIDs($daemons, $pids); - - if (!$stop_pids) { - $console->writeErr("%s\n", pht('No daemons to kill.')); - return 0; - } - - $survivors = $this->sendStopSignals($stop_pids, $grace_period); - - // Try to clean up PID files for daemons we killed. - $remove = array(); - foreach ($daemons as $daemon) { - $pid = $daemon->getPID(); - if (empty($stop_pids[$pid])) { - // We did not try to stop this overseer. - continue; - } - - if (isset($survivors[$pid])) { - // We weren't able to stop this overseer. - continue; - } - - if (!$daemon->getPIDFile()) { - // We don't know where the PID file is. - continue; - } - - $remove[] = $daemon->getPIDFile(); - } - - foreach (array_unique($remove) as $remove_file) { - Filesystem::remove($remove_file); - } - - if (!$gently) { - $this->processRogueDaemons($grace_period, !$pids, $force); + if ($live_pids) { + $this->logWarn( + pht('SURVIVORS'), + pht( + 'Unable to stop all daemon processes. You may need to run this '. + 'command as root with "sudo".')); } return 0; @@ -492,42 +500,6 @@ abstract class PhabricatorDaemonManagementWorkflow return 0; } - private function processRogueDaemons($grace_period, $warn, $force_stop) { - $console = PhutilConsole::getConsole(); - - $rogue_daemons = PhutilDaemonOverseer::findRunningDaemons(); - if ($rogue_daemons) { - if ($force_stop) { - $rogue_pids = ipull($rogue_daemons, 'pid'); - $survivors = $this->sendStopSignals($rogue_pids, $grace_period); - if ($survivors) { - $console->writeErr( - "%s\n", - pht( - 'Unable to stop processes running without PID files. '. - 'Try running this command again with sudo.')); - } - } else if ($warn) { - $console->writeErr("%s\n", $this->getForceStopHint($rogue_daemons)); - } - } - - return $rogue_daemons; - } - - private function getForceStopHint($rogue_daemons) { - $debug_output = ''; - foreach ($rogue_daemons as $rogue) { - $debug_output .= $rogue['pid'].' '.$rogue['command']."\n"; - } - return pht( - "There are processes running that look like Phabricator daemons but ". - "have no corresponding PID files:\n\n%s\n\n". - "Stop these processes by re-running this command with the %s parameter.", - $debug_output, - '--force'); - } - private function sendStopSignals($pids, $grace_period) { // If we're doing a graceful shutdown, try SIGINT first. if ($grace_period) { From b99c240aa3749f0f3042fdf8c2a7553a39dee7c1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 14:21:06 -0700 Subject: [PATCH 007/122] Deprecate "bin/phd ... --gently" and update documentation Summary: Ref T13321. Previously, the behavior was: - `bin/phd stop --gently`: Stop all daemons with PID files that belong to the current instance. - `bin/phd stop`: Stop all daemons with PID files that belong to the current instance. Complain if there are more processes. - `bin/phd stop --force`: Stop all processes that look like daemons, ignoring instances. The new behavior is: - `bin/phd stop`: Stop all processes that look like daemons and belong to the current instance. - `bin/phd stop --force`: Stop all processes that look like daemons, period. Test Plan: Grep / documentation only. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13321 Differential Revision: https://secure.phabricator.com/D20602 --- ...ricatorDaemonManagementRestartWorkflow.php | 21 ++++++++++--------- ...habricatorDaemonManagementStopWorkflow.php | 19 ++++------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php index 5a1b46ef1a..eb2d5d229c 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementRestartWorkflow.php @@ -6,7 +6,10 @@ final class PhabricatorDaemonManagementRestartWorkflow protected function didConstruct() { $this ->setName('restart') - ->setSynopsis(pht('Stop, then start the standard daemon loadout.')) + ->setSynopsis( + pht( + 'Stop daemon processes on this host, then start the standard '. + 'daemon loadout.')) ->setArguments( array( array( @@ -17,17 +20,15 @@ final class PhabricatorDaemonManagementRestartWorkflow 'seconds. Defaults to __15__ seconds.'), 'default' => 15, ), - array( - 'name' => 'gently', - 'help' => pht( - 'Ignore running processes that look like daemons but do not '. - 'have corresponding PID files.'), - ), array( 'name' => 'force', 'help' => pht( - 'Also stop running processes that look like daemons but do '. - 'not have corresponding PID files.'), + 'Stop all daemon processes on this host, even if they belong '. + 'to another Phabricator instance.'), + ), + array( + 'name' => 'gently', + 'help' => pht('Deprecated. Has no effect.'), ), $this->getAutoscaleReserveArgument(), )); @@ -38,8 +39,8 @@ final class PhabricatorDaemonManagementRestartWorkflow array( 'graceful' => $args->getArg('graceful'), 'force' => $args->getArg('force'), - 'gently' => $args->getArg('gently'), )); + if ($err) { return $err; } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php index 6f982f3e42..19b9fc44fb 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStopWorkflow.php @@ -6,11 +6,7 @@ final class PhabricatorDaemonManagementStopWorkflow protected function didConstruct() { $this ->setName('stop') - ->setSynopsis( - pht( - 'Stop all running daemons, or specific daemons identified by PIDs. '. - 'Use **%s** to find PIDs.', - 'phd status')) + ->setSynopsis(pht('Stop daemon processes on this host.')) ->setArguments( array( array( @@ -24,18 +20,12 @@ final class PhabricatorDaemonManagementStopWorkflow array( 'name' => 'force', 'help' => pht( - 'Also stop running processes that look like daemons but do '. - 'not have corresponding PID files.'), + 'Stop all daemon processes on this host, even if they belong '. + 'to another Phabricator instance.'), ), array( 'name' => 'gently', - 'help' => pht( - 'Ignore running processes that look like daemons but do not '. - 'have corresponding PID files.'), - ), - array( - 'name' => 'pids', - 'wildcard' => true, + 'help' => pht('Deprecated. Has no effect.'), ), )); } @@ -45,7 +35,6 @@ final class PhabricatorDaemonManagementStopWorkflow array( 'graceful' => $args->getArg('graceful'), 'force' => $args->getArg('force'), - 'gently' => $args->getArg('gently'), )); } From 08b9e70bea5c3d2e18dfc8601cbe4027cff9df45 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 14:32:06 -0700 Subject: [PATCH 008/122] Make "bin/phd status" report local daemons from the process list, not a mess of local/remote information Summary: Ref T13321. Fixes T11037. Realign "bin/phd status" to just mean "show daemon processes on this host". The value of `bin/phd status` as a mixed remote/local command isn't clear, and the current output is a confusing mess (see T11037). This also continues letting us move away from PID files. Test Plan: Ran `bin/phd status`, saw sensible local process status. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13321, T11037 Differential Revision: https://secure.phabricator.com/D20604 --- ...bricatorDaemonManagementStatusWorkflow.php | 115 ++++++------------ .../PhabricatorDaemonManagementWorkflow.php | 25 ---- 2 files changed, 36 insertions(+), 104 deletions(-) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php index 343e42ba63..14854b5881 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -6,101 +6,58 @@ final class PhabricatorDaemonManagementStatusWorkflow protected function didConstruct() { $this ->setName('status') - ->setSynopsis(pht('Show status of running daemons.')) - ->setArguments( - array( - array( - 'name' => 'local', - 'help' => pht('Show only local daemons.'), - ), - )); + ->setSynopsis(pht('Show daemon processes on this host.')); } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); + $query = id(new PhutilProcessQuery()) + ->withIsOverseer(true); - if ($args->getArg('local')) { - $daemons = $this->loadRunningDaemons(); - } else { - $daemons = $this->loadAllRunningDaemons(); + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + if ($instance !== null) { + $query->withInstances(array($instance)); } - if (!$daemons) { - $console->writeErr( - "%s\n", - pht('There are no running Phabricator daemons.')); + $process_refs = $query->execute(); + if (!$process_refs) { + if ($instance !== null) { + $this->logInfo( + pht('NO DAEMONS'), + pht( + 'There are no running daemon processes for the current '. + 'instance ("%s").', + $instance)); + } else { + $this->writeInfo( + pht('NO DAEMONS'), + pht('There are no running daemon processes.')); + } + return 1; } - $status = 0; - $table = id(new PhutilConsoleTable()) - ->addColumns(array( - 'id' => array( - 'title' => pht('Log'), - ), - 'daemonID' => array( - 'title' => pht('Daemon'), - ), - 'host' => array( - 'title' => pht('Host'), - ), - 'pid' => array( - 'title' => pht('Overseer'), - ), - 'started' => array( - 'title' => pht('Started'), - ), - 'daemon' => array( - 'title' => pht('Class'), - ), - 'argv' => array( - 'title' => pht('Arguments'), - ), - )); - - foreach ($daemons as $daemon) { - if ($daemon instanceof PhabricatorDaemonLog) { - $table->addRow(array( - 'id' => $daemon->getID(), - 'daemonID' => $daemon->getDaemonID(), - 'host' => $daemon->getHost(), - 'pid' => $daemon->getPID(), - 'started' => date('M j Y, g:i:s A', $daemon->getDateCreated()), - 'daemon' => $daemon->getDaemon(), - 'argv' => csprintf('%LR', $daemon->getExplicitArgv()), + ->addColumns( + array( + 'pid' => array( + 'title' => pht('PID'), + ), + 'command' => array( + 'title' => pht('Command'), + ), )); - } else if ($daemon instanceof PhabricatorDaemonReference) { - $name = $daemon->getName(); - if (!$daemon->isRunning()) { - $daemon->updateStatus(PhabricatorDaemonLog::STATUS_DEAD); - $status = 2; - $name = pht(' %s', $name); - } - $daemon_log = $daemon->getDaemonLog(); - $id = null; - $daemon_id = null; - if ($daemon_log) { - $id = $daemon_log->getID(); - $daemon_id = $daemon_log->getDaemonID(); - } - - $table->addRow(array( - 'id' => $id, - 'daemonID' => $daemon_id, - 'host' => 'localhost', - 'pid' => $daemon->getPID(), - 'started' => $daemon->getEpochStarted() - ? date('M j Y, g:i:s A', $daemon->getEpochStarted()) - : null, - 'daemon' => $name, - 'argv' => csprintf('%LR', $daemon->getArgv()), + foreach ($process_refs as $process_ref) { + $table->addRow( + array( + 'pid' => $process_ref->getPID(), + 'command' => $process_ref->getCommand(), )); - } } $table->draw(); + + return 0; } } diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index e09376195b..fded86bb7c 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -55,31 +55,6 @@ abstract class PhabricatorDaemonManagementWorkflow return array_mergev($daemons); } - final protected function loadAllRunningDaemons() { - $local_daemons = $this->loadRunningDaemons(); - - $local_ids = array(); - foreach ($local_daemons as $daemon) { - $daemon_log = $daemon->getDaemonLog(); - - if ($daemon_log) { - $local_ids[] = $daemon_log->getID(); - } - } - - $daemon_query = id(new PhabricatorDaemonLogQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE); - - if ($local_ids) { - $daemon_query->withoutIDs($local_ids); - } - - $remote_daemons = $daemon_query->execute(); - - return array_merge($local_daemons, $remote_daemons); - } - private function findDaemonClass($substring) { $symbols = $this->loadAvailableDaemonClasses(); From 2498e373b955e74e46ca4725946542666ae7cdd2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 14:48:07 -0700 Subject: [PATCH 009/122] Make "phd start" and "phd reload" use the process list, not PID files Summary: Ref T13321. This gets rid of the last pidfile readers in Phabricator; we just use the process list instead. These commands always only work on the current instance since they don't make much sense otherwise. Test Plan: Ran `bin/phd start` and `bin/phd reload` with and without daemons running. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13321 Differential Revision: https://secure.phabricator.com/D20606 --- ...bricatorDaemonManagementStatusWorkflow.php | 10 +- .../PhabricatorDaemonManagementWorkflow.php | 105 +++++++++--------- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php index 14854b5881..1f7ed951cb 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementStatusWorkflow.php @@ -10,16 +10,10 @@ final class PhabricatorDaemonManagementStatusWorkflow } public function execute(PhutilArgumentParser $args) { - $query = id(new PhutilProcessQuery()) - ->withIsOverseer(true); + $process_refs = $this->getOverseerProcessRefs(); - $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); - if ($instance !== null) { - $query->withInstances(array($instance)); - } - - $process_refs = $query->execute(); if (!$process_refs) { + $instance = $this->getInstance(); if ($instance !== null) { $this->logInfo( pht('NO DAEMONS'), diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index fded86bb7c..48129ac13a 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -41,20 +41,6 @@ abstract class PhabricatorDaemonManagementWorkflow return $path; } - final protected function loadRunningDaemons() { - $daemons = array(); - - $pid_dir = $this->getPIDDirectory(); - $pid_files = Filesystem::listDirectory($pid_dir); - - foreach ($pid_files as $pid_file) { - $path = $pid_dir.'/'.$pid_file; - $daemons[] = PhabricatorDaemonReference::loadReferencesFromFile($path); - } - - return array_mergev($daemons); - } - private function findDaemonClass($substring) { $symbols = $this->loadAvailableDaemonClasses(); @@ -144,7 +130,7 @@ abstract class PhabricatorDaemonManagementWorkflow $flags[] = '--verbose'; } - $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + $instance = $this->getInstance(); if ($instance) { $flags[] = '-l'; $flags[] = $instance; @@ -299,28 +285,31 @@ abstract class PhabricatorDaemonManagementWorkflow $console = PhutilConsole::getConsole(); if (!idx($options, 'force')) { - $running = $this->loadRunningDaemons(); + $process_refs = $this->getOverseerProcessRefs(); + if ($process_refs) { + $this->logWarn( + pht('RUNNING DAEMONS'), + pht('Daemons are already running:')); - // This may include daemons which were launched but which are no longer - // running; check that we actually have active daemons before failing. - foreach ($running as $daemon) { - if ($daemon->isRunning()) { - $message = pht( - "phd start: Unable to start daemons because daemons are already ". - "running.\n\n". - "You can view running daemons with '%s'.\n". - "You can stop running daemons with '%s'.\n". - "You can use '%s' to stop all daemons before starting ". - "new daemons.\n". - "You can force daemons to start anyway with %s.", - 'phd status', - 'phd stop', - 'phd restart', - '--force'); - - $console->writeErr("%s\n", $message); - exit(1); + fprintf(STDERR, '%s', "\n"); + foreach ($process_refs as $process_ref) { + fprintf( + STDERR, + '%s', + tsprintf( + " %s %s\n", + $process_ref->getPID(), + $process_ref->getCommand())); } + fprintf(STDERR, '%s', "\n"); + + $this->logFail( + pht('RUNNING DAEMONS'), + pht( + 'Use "phd stop" to stop daemons, "phd restart" to restart '. + 'daemons, or "phd start --force" to ignore running processes.')); + + exit(1); } } @@ -368,7 +357,7 @@ abstract class PhabricatorDaemonManagementWorkflow $query = id(new PhutilProcessQuery()) ->withIsOverseer(true); - $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + $instance = $this->getInstance(); if ($instance !== null && !$force) { $query->withInstances(array($instance)); } @@ -447,28 +436,23 @@ abstract class PhabricatorDaemonManagementWorkflow } final protected function executeReloadCommand(array $pids) { - $console = PhutilConsole::getConsole(); + $process_refs = $this->getOverseerProcessRefs(); + + if (!$process_refs) { + $this->logInfo( + pht('NO DAEMONS'), + pht('There are no running daemon processes to reload.')); - $daemons = $this->loadRunningDaemons(); - if (!$daemons) { - $console->writeErr( - "%s\n", - pht('There are no running daemons to reload.')); return 0; } - $reload_pids = $this->selectDaemonPIDs($daemons, $pids); - if (!$reload_pids) { - $console->writeErr( - "%s\n", - pht('No daemons to reload.')); - return 0; - } + foreach ($process_refs as $process_ref) { + $pid = $process_ref->getPID(); - foreach ($reload_pids as $pid) { - $console->writeOut( - "%s\n", + $this->logInfo( + pht('RELOAD'), pht('Reloading process %d...', $pid)); + posix_kill($pid, SIGHUP); } @@ -621,4 +605,21 @@ abstract class PhabricatorDaemonManagementWorkflow return $select_pids; } + protected function getOverseerProcessRefs() { + $query = id(new PhutilProcessQuery()) + ->withIsOverseer(true); + + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + if ($instance !== null) { + $query->withInstances(array($instance)); + } + + return $query->execute(); + } + + protected function getInstance() { + return PhabricatorEnv::getEnvConfig('cluster.instance'); + } + + } From 65bc481c91de53fdcc7368f4563cdadb1fe86629 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Jun 2019 15:09:39 -0700 Subject: [PATCH 010/122] Remove "phd.pid-directory" configuration and stop passing "piddir" to daemons Summary: Ref T13321. The daemons no longer write PID files, so we no longer need to pass any of this stuff to them. Test Plan: Grepped for affected symbols. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13321 Differential Revision: https://secure.phabricator.com/D20608 --- .../PhabricatorExtraConfigSetupCheck.php | 3 + .../option/PhabricatorPHDConfigOptions.php | 4 - .../PhabricatorDaemonManagementWorkflow.php | 16 +-- .../control/PhabricatorDaemonReference.php | 135 +----------------- 4 files changed, 7 insertions(+), 151 deletions(-) diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index a8a883ca96..d863c928b7 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -536,6 +536,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'differential.whitespace-matters' => pht( 'Whitespace rendering is now handled automatically.'), + + 'phd.pid-directory' => pht( + 'Phabricator daemons no longer use PID files.'), ); return $ancient_config; diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php index e04353876a..7a1d39e617 100644 --- a/src/applications/config/option/PhabricatorPHDConfigOptions.php +++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php @@ -21,10 +21,6 @@ final class PhabricatorPHDConfigOptions public function getOptions() { return array( - $this->newOption('phd.pid-directory', 'string', '/var/tmp/phd/pid') - ->setLocked(true) - ->setDescription( - pht('Directory that phd should use to track running daemons.')), $this->newOption('phd.log-directory', 'string', '/var/tmp/phd/log') ->setLocked(true) ->setDescription( diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 48129ac13a..05d94e218d 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -12,11 +12,6 @@ abstract class PhabricatorDaemonManagementWorkflow ->selectSymbolsWithoutLoading(); } - final protected function getPIDDirectory() { - $path = PhabricatorEnv::getEnvConfig('phd.pid-directory'); - return $this->getControlDirectory($path); - } - final protected function getLogDirectory() { $path = PhabricatorEnv::getEnvConfig('phd.log-directory'); return $this->getControlDirectory($path); @@ -30,11 +25,10 @@ abstract class PhabricatorDaemonManagementWorkflow pht( "%s requires the directory '%s' to exist, but it does not exist ". "and could not be created. Create this directory or update ". - "'%s' / '%s' in your configuration to point to an existing ". + "'%s' in your configuration to point to an existing ". "directory.", 'phd', $path, - 'phd.pid-directory', 'phd.log-directory')); } } @@ -146,14 +140,6 @@ abstract class PhabricatorDaemonManagementWorkflow $config['log'] = $this->getLogDirectory().'/daemons.log'; } - $pid_dir = $this->getPIDDirectory(); - - // TODO: This should be a much better user experience. - Filesystem::assertExists($pid_dir); - Filesystem::assertIsDirectory($pid_dir); - Filesystem::assertWritable($pid_dir); - - $config['piddir'] = $pid_dir; $config['daemons'] = $daemons; $command = csprintf('./phd-daemon %Ls', $flags); diff --git a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php index d9f8180935..6c90e1eed4 100644 --- a/src/infrastructure/daemon/control/PhabricatorDaemonReference.php +++ b/src/infrastructure/daemon/control/PhabricatorDaemonReference.php @@ -1,128 +1,10 @@ setViewer(PhabricatorUser::getOmnipotentUser()) - ->withDaemonIDs($daemon_ids) - ->execute(); - } catch (AphrontQueryException $ex) { - // Ignore any issues here; getting this information only allows us - // to provide a more complete picture of daemon status, and we want - // these commands to work if the database is inaccessible. - } - - $logs = mpull($logs, null, 'getDaemonID'); - } - - // Support PID files that use the old daemon format, where each overseer - // had exactly one daemon. We can eventually remove this; they will still - // be stopped by `phd stop --force` even if we don't identify them here. - if (!$daemons && idx($dict, 'name')) { - $daemons = array( - array( - 'config' => array( - 'class' => idx($dict, 'name'), - 'argv' => idx($dict, 'argv', array()), - ), - ), - ); - } - - foreach ($daemons as $daemon) { - $ref = new PhabricatorDaemonReference(); - - // NOTE: This is the overseer PID, not the actual daemon process PID. - // This is correct for checking status and sending signals (the only - // things we do with it), but might be confusing. $daemon['pid'] has - // the daemon PID, and we could expose that if we had some use for it. - - $ref->pid = idx($dict, 'pid'); - $ref->start = idx($dict, 'start'); - - $config = idx($daemon, 'config', array()); - $ref->name = idx($config, 'class'); - $ref->argv = idx($config, 'argv', array()); - - $log = idx($logs, idx($daemon, 'id')); - if ($log) { - $ref->daemonLog = $log; - } - - $ref->pidFile = $path; - $refs[] = $ref; - } - - return $refs; - } - - public function updateStatus($new_status) { - if (!$this->daemonLog) { - return; - } - - try { - $this->daemonLog - ->setStatus($new_status) - ->save(); - } catch (AphrontQueryException $ex) { - // Ignore anything that goes wrong here. - } - } - - public function getPID() { - return $this->pid; - } - - public function getName() { - return $this->name; - } - - public function getArgv() { - return $this->argv; - } - - public function getEpochStarted() { - return $this->start; - } - - public function getPIDFile() { - return $this->pidFile; - } - - public function getDaemonLog() { - return $this->daemonLog; - } - - public function isRunning() { - return self::isProcessRunning($this->getPID()); - } - public static function isProcessRunning($pid) { if (!$pid) { return false; @@ -148,15 +30,4 @@ final class PhabricatorDaemonReference extends Phobject { return $is_running; } - public function waitForExit($seconds) { - $start = time(); - while (time() < $start + $seconds) { - usleep(100000); - if (!$this->isRunning()) { - return true; - } - } - return !$this->isRunning(); - } - } From a3397fb8761355201e077efc1261379a9f6c1eb5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 24 Jun 2019 14:48:17 -0700 Subject: [PATCH 011/122] Consider "all account members are disabled" to be a permanent failure when billing a Phortune subscription Summary: Fixes T13327. Currently, when we try to bill an account and all members are disabled, we fail temporarily and the task retries forever. At least for now, just treat this as a permanent failure. Test Plan: - Used `bin/phortune invoice` to generate a normal invoice for a regular subscription. - Disabled all the account members, then tried again. Got a helpful permanent failure: ``` $ ./bin/phortune invoice --subscription PHID-PSUB-kbedwt5cyepoc6tohjq5 --auto-range Set current time to Mon, Jun 24, 2:47 PM. Preparing to invoice subscription "localb.phacility.com" from Fri, May 31, 10:14 AM to Sun, Jun 30, 10:14 AM. WARNING Manually invoicing will double bill payment accounts if the range overlaps an existing or future invoice. This script is intended for testing and development, and should not be part of routine billing operations. If you continue, you may incorrectly overcharge customers. Really invoice this subscription? [y/N] y [2019-06-24 14:47:57] EXCEPTION: (PhabricatorWorkerPermanentFailureException) All members of the account ("PHID-ACNT-qp54y3unedoaxgkkjpj4") for this subscription ("PHID-PSUB-kbedwt5cyepoc6tohjq5") are disabled. at [/src/applications/phortune/worker/PhortuneSubscriptionWorker.php:88] arcanist(head=experimental, ref.master=d92fa96366c0, ref.experimental=db4cd55d4673), corgi(head=master, ref.master=6371578c9d32), instances(head=stable, ref.master=ba9e4a19df1c, ref.stable=37fb1f4917c7), libcore(), phabricator(head=master, ref.master=65bc481c91de, custom=11), phutil(head=master, ref.master=7adfe4e4f4a3), services(head=master, ref.master=5424383159ac) #0 PhortuneSubscriptionWorker::doWork() called at [/src/infrastructure/daemon/workers/PhabricatorWorker.php:124] #1 PhabricatorWorker::executeTask() called at [/src/infrastructure/daemon/workers/PhabricatorWorker.php:163] #2 PhabricatorWorker::scheduleTask(string, array, array) called at [/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php:169] #3 PhabricatorPhortuneManagementInvoiceWorkflow::execute(PhutilArgumentParser) called at [/src/parser/argument/PhutilArgumentParser.php:457] #4 PhutilArgumentParser::parseWorkflowsFull(array) called at [/src/parser/argument/PhutilArgumentParser.php:349] #5 PhutilArgumentParser::parseWorkflows(array) called at [/scripts/setup/manage_phortune.php:21] $ ``` Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13327 Differential Revision: https://secure.phabricator.com/D20613 --- .../worker/PhortuneSubscriptionWorker.php | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php index d05aacbb7c..1c9a3f0fa0 100644 --- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php +++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php @@ -48,12 +48,15 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { ->withPHIDs($account->getMemberPHIDs()) ->execute(); $actor = null; + + $any_disabled = false; foreach ($members as $member) { // Don't act as a disabled user. If all of the users on the account are // disabled this means we won't charge the subscription, but that's // probably correct since it means no one can cancel or pay it anyway. if ($member->getIsDisabled()) { + $any_disabled = true; continue; } @@ -63,7 +66,26 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { } if (!$actor) { - throw new Exception(pht('Failed to load actor to bill subscription!')); + if ($any_disabled) { + $message = pht( + 'All members of the account ("%s") for this subscription ("%s") '. + 'are disabled.', + $account->getPHID(), + $subscription->getPHID()); + } else if ($account->getMemberPHIDs()) { + $message = pht( + 'Unable to load any of the members of the account ("%s") for this '. + 'subscription ("%s").', + $account->getPHID(), + $subscription->getPHID()); + } else { + $message = pht( + 'The account ("%s") for this subscription ("%s") has no '. + 'members.', + $account->getPHID(), + $subscription->getPHID()); + } + throw new PhabricatorWorkerPermanentFailureException($message); } $cart = $account->newCart($actor, $cart_implementation, $merchant); From da0dfc057d648ecb4425d40a85937e9ca149fec2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 24 Jun 2019 15:09:57 -0700 Subject: [PATCH 012/122] Make "bin/files" parsing of working set arguments more consistent Summary: Fixes T13326. In D20571, I slightly generalized construction of an iterator over a set of files, but missed some code in other "bin/files ..." commands which was also affected. Today, basically all of these workflows define their own "--all" and "names" flags. Pull these definitions up and implement them more consistently. Test Plan: Ran multiple different `bin/files` commands with different combinations of arguments, saw consistent handling of iterator construction. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13326 Differential Revision: https://secure.phabricator.com/D20614 --- ...bricatorFilesManagementCompactWorkflow.php | 30 ++--- ...habricatorFilesManagementCycleWorkflow.php | 30 ++--- ...abricatorFilesManagementEncodeWorkflow.php | 55 ++++----- ...icatorFilesManagementIntegrityWorkflow.php | 90 +++++++-------- ...bricatorFilesManagementMigrateWorkflow.php | 105 ++++++++---------- ...bricatorFilesManagementRebuildWorkflow.php | 48 +++----- .../PhabricatorFilesManagementWorkflow.php | 30 ++++- 7 files changed, 166 insertions(+), 222 deletions(-) diff --git a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php index 0c8e7153e5..5d8115389b 100644 --- a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php @@ -4,41 +4,25 @@ final class PhabricatorFilesManagementCompactWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + $arguments[] = array( + 'name' => 'dry-run', + 'help' => pht('Show what would be compacted.'), + ); + $this ->setName('compact') ->setSynopsis( pht( 'Merge identical files to share the same storage. In some cases, '. 'this can repair files with missing data.')) - ->setArguments( - array( - array( - 'name' => 'dry-run', - 'help' => pht('Show what would be compacted.'), - ), - array( - 'name' => 'all', - 'help' => pht('Compact all files.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to compact, or use `%s` '. - 'to compact all files.', - '--all')); - } - $is_dry_run = $args->getArg('dry-run'); foreach ($iterator as $file) { diff --git a/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php index 6d574d633a..cbcfac2cc8 100644 --- a/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementCycleWorkflow.php @@ -4,36 +4,22 @@ final class PhabricatorFilesManagementCycleWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + $arguments[] = array( + 'name' => 'key', + 'param' => 'keyname', + 'help' => pht('Select a specific storage key to cycle to.'), + ); + $this ->setName('cycle') ->setSynopsis( pht('Cycle master key for encrypted files.')) - ->setArguments( - array( - array( - 'name' => 'key', - 'param' => 'keyname', - 'help' => pht('Select a specific storage key to cycle to.'), - ), - array( - 'name' => 'all', - 'help' => pht('Change encoding for all files.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to cycle, or use --all to cycle '. - 'all files.')); - } $format_map = PhabricatorFileStorageFormat::getAllFormats(); $engines = PhabricatorFileStorageEngine::loadAllEngines(); diff --git a/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php index 1d972326da..f7d299bc5f 100644 --- a/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php @@ -4,47 +4,36 @@ final class PhabricatorFilesManagementEncodeWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'as', + 'param' => 'format', + 'help' => pht('Select the storage format to use.'), + ); + + $arguments[] = array( + 'name' => 'key', + 'param' => 'keyname', + 'help' => pht('Select a specific storage key.'), + ); + + $arguments[] = array( + 'name' => 'force', + 'help' => pht( + 'Re-encode files which are already stored in the target '. + 'encoding.'), + ); + $this ->setName('encode') ->setSynopsis( pht('Change the storage encoding of files.')) - ->setArguments( - array( - array( - 'name' => 'as', - 'param' => 'format', - 'help' => pht('Select the storage format to use.'), - ), - array( - 'name' => 'key', - 'param' => 'keyname', - 'help' => pht('Select a specific storage key.'), - ), - array( - 'name' => 'all', - 'help' => pht('Change encoding for all files.'), - ), - array( - 'name' => 'force', - 'help' => pht( - 'Re-encode files which are already stored in the target '. - 'encoding.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to encode, or use --all to '. - 'encode all files.')); - } $force = (bool)$args->getArg('force'); diff --git a/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php index 344df71460..a30f4f970b 100644 --- a/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php @@ -4,52 +4,50 @@ final class PhabricatorFilesManagementIntegrityWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'strip', + 'help' => pht( + 'DANGEROUS. Strip integrity hashes from files. This makes '. + 'files vulnerable to corruption or tampering.'), + ); + + $arguments[] = array( + 'name' => 'corrupt', + 'help' => pht( + 'Corrupt integrity hashes for given files. This is intended '. + 'for debugging.'), + ); + + $arguments[] = array( + 'name' => 'compute', + 'help' => pht( + 'Compute and update integrity hashes for files which do not '. + 'yet have them.'), + ); + + $arguments[] = array( + 'name' => 'overwrite', + 'help' => pht( + 'DANGEROUS. Recompute and update integrity hashes, overwriting '. + 'invalid hashes. This may mark corrupt or dangerous files as '. + 'valid.'), + ); + + $arguments[] = array( + 'name' => 'force', + 'short' => 'f', + 'help' => pht( + 'Execute dangerous operations without prompting for '. + 'confirmation.'), + ); + + $this ->setName('integrity') ->setSynopsis(pht('Verify or recalculate file integrity hashes.')) - ->setArguments( - array( - array( - 'name' => 'all', - 'help' => pht('Affect all files.'), - ), - array( - 'name' => 'strip', - 'help' => pht( - 'DANGEROUS. Strip integrity hashes from files. This makes '. - 'files vulnerable to corruption or tampering.'), - ), - array( - 'name' => 'corrupt', - 'help' => pht( - 'Corrupt integrity hashes for given files. This is intended '. - 'for debugging.'), - ), - array( - 'name' => 'compute', - 'help' => pht( - 'Compute and update integrity hashes for files which do not '. - 'yet have them.'), - ), - array( - 'name' => 'overwrite', - 'help' => pht( - 'DANGEROUS. Recompute and update integrity hashes, overwriting '. - 'invalid hashes. This may mark corrupt or dangerous files as '. - 'valid.'), - ), - array( - 'name' => 'force', - 'short' => 'f', - 'help' => pht( - 'Execute dangerous operations without prompting for '. - 'confirmation.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { @@ -120,12 +118,6 @@ final class PhabricatorFilesManagementIntegrityWorkflow } $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to affect, or use "--all" to '. - 'affect all files.')); - } $failure_count = 0; $total_count = 0; diff --git a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php index 58d5155aed..859ee0bf2d 100644 --- a/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php @@ -4,61 +4,54 @@ final class PhabricatorFilesManagementMigrateWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'engine', + 'param' => 'storage-engine', + 'help' => pht('Migrate to the named storage engine.'), + ); + + $arguments[] = array( + 'name' => 'dry-run', + 'help' => pht('Show what would be migrated.'), + ); + + $arguments[] = array( + 'name' => 'min-size', + 'param' => 'bytes', + 'help' => pht( + 'Do not migrate data for files which are smaller than a given '. + 'filesize.'), + ); + + $arguments[] = array( + 'name' => 'max-size', + 'param' => 'bytes', + 'help' => pht( + 'Do not migrate data for files which are larger than a given '. + 'filesize.'), + ); + + $arguments[] = array( + 'name' => 'copy', + 'help' => pht( + 'Copy file data instead of moving it: after migrating, do not '. + 'remove the old data even if it is no longer referenced.'), + ); + + $arguments[] = array( + 'name' => 'local-disk-source', + 'param' => 'path', + 'help' => pht( + 'When migrating from a local disk source, use the specified '. + 'path as the root directory.'), + ); + $this ->setName('migrate') ->setSynopsis(pht('Migrate files between storage engines.')) - ->setArguments( - array( - array( - 'name' => 'engine', - 'param' => 'storage_engine', - 'help' => pht('Migrate to the named storage engine.'), - ), - array( - 'name' => 'dry-run', - 'help' => pht('Show what would be migrated.'), - ), - array( - 'name' => 'min-size', - 'param' => 'bytes', - 'help' => pht( - 'Do not migrate data for files which are smaller than a given '. - 'filesize.'), - ), - array( - 'name' => 'max-size', - 'param' => 'bytes', - 'help' => pht( - 'Do not migrate data for files which are larger than a given '. - 'filesize.'), - ), - array( - 'name' => 'all', - 'help' => pht('Migrate all files.'), - ), - array( - 'name' => 'copy', - 'help' => pht( - 'Copy file data instead of moving it: after migrating, do not '. - 'remove the old data even if it is no longer referenced.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - array( - 'name' => 'from-engine', - 'param' => 'engine', - 'help' => pht('Migrate files from the named storage engine.'), - ), - array( - 'name' => 'local-disk-source', - 'param' => 'path', - 'help' => pht( - 'When migrating from a local disk source, use the specified '. - 'path as the root directory.'), - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { @@ -97,14 +90,6 @@ final class PhabricatorFilesManagementMigrateWorkflow $target_engine = PhabricatorFile::buildEngine($target_key); $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to migrate, or use `%s` '. - 'to migrate all files.', - '--all')); - } - $is_dry_run = $args->getArg('dry-run'); $min_size = (int)$args->getArg('min-size'); diff --git a/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php index f7fc890ae4..e577a04d55 100644 --- a/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php @@ -4,45 +4,33 @@ final class PhabricatorFilesManagementRebuildWorkflow extends PhabricatorFilesManagementWorkflow { protected function didConstruct() { + $arguments = $this->newIteratorArguments(); + + $arguments[] = array( + 'name' => 'dry-run', + 'help' => pht('Show what would be updated.'), + ); + + $arguments[] = array( + 'name' => 'rebuild-mime', + 'help' => pht('Rebuild MIME information.'), + ); + + $arguments[] = array( + 'name' => 'rebuild-dimensions', + 'help' => pht('Rebuild image dimension information.'), + ); + $this ->setName('rebuild') ->setSynopsis(pht('Rebuild metadata of old files.')) - ->setArguments( - array( - array( - 'name' => 'all', - 'help' => pht('Update all files.'), - ), - array( - 'name' => 'dry-run', - 'help' => pht('Show what would be updated.'), - ), - array( - 'name' => 'rebuild-mime', - 'help' => pht('Rebuild MIME information.'), - ), - array( - 'name' => 'rebuild-dimensions', - 'help' => pht('Rebuild image dimension information.'), - ), - array( - 'name' => 'names', - 'wildcard' => true, - ), - )); + ->setArguments($arguments); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of files to update, or use `%s` '. - 'to update all files.', - '--all')); - } $update = array( 'mime' => $args->getArg('rebuild-mime'), diff --git a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php index 44d43dc66a..6debf9c711 100644 --- a/src/applications/files/management/PhabricatorFilesManagementWorkflow.php +++ b/src/applications/files/management/PhabricatorFilesManagementWorkflow.php @@ -3,11 +3,30 @@ abstract class PhabricatorFilesManagementWorkflow extends PhabricatorManagementWorkflow { + protected function newIteratorArguments() { + return array( + array( + 'name' => 'all', + 'help' => pht('Operate on all files.'), + ), + array( + 'name' => 'names', + 'wildcard' => true, + ), + array( + 'name' => 'from-engine', + 'param' => 'storage-engine', + 'help' => pht('Operate on files stored in a specified engine.'), + ), + ); + } + protected function buildIterator(PhutilArgumentParser $args) { $viewer = $this->getViewer(); - $names = $args->getArg('names'); $is_all = $args->getArg('all'); + + $names = $args->getArg('names'); $from_engine = $args->getArg('from-engine'); $any_constraint = ($from_engine || $names); @@ -15,15 +34,16 @@ abstract class PhabricatorFilesManagementWorkflow if (!$is_all && !$any_constraint) { throw new PhutilArgumentUsageException( pht( - 'Use "--all" to migrate all files, or choose files to migrate '. - 'with "--names" or "--from-engine".')); + 'Specify which files to operate on, or use "--all" to operate on '. + 'all files.')); } if ($is_all && $any_constraint) { throw new PhutilArgumentUsageException( pht( - 'You can not migrate all files with "--all" and also migrate only '. - 'a subset of files with "--from-engine" or "--names".')); + 'You can not operate on all files with "--all" and also operate '. + 'on a subset of files by naming them explicitly or using '. + 'constraint flags like "--from-engine".')); } // If we're migrating specific named files, convert the names into IDs From eaa60334ec16ab8536d4a3d31b75a27c09fec796 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 25 Jun 2019 05:18:31 -0700 Subject: [PATCH 013/122] Limit the read buffer size in `bin/storage dump` Summary: Ref T13328. Currently, we read from `mysqldump` something like this: ``` until (done) { for (100 ms) { mysqldump > in-memory-buffer; } in-memory-buffer > disk; } ``` This general structure isn't great. In this use case, where we're streaming a large amount of data from a source to a sink, we'd prefer to have a "select()"-like way to interact with futures, so our code is called after every read (or maybe once some small buffer fills up, if we want to do the writes in larger chunks). We don't currently have this (`FutureIterator` can wake up every X milliseconds, or on future exit, but, today, can not wake for readable futures), so we may buffer an arbitrary amount of data into memory (however much data `mysqldump` can write in 100ms). Reduce the update frequency from 100ms to 10ms, and limit the buffer size to 32MB. This effectively imposes an artificial 3,200MB/sec limit on throughput, but hopefully that's fast enough that we'll have a "wake on readable" mechanism by the time it's a problem. Test Plan: - Replaced `mysqldump` with `cat /dev/zero` as the source command, to get fast input. - Ran `bin/storage dump` with `var_dump()` on the buffer size. - Before change: saw arbitrarily large buffers (300MB+). - After change: saw consistent maximum buffer size of 32MB. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13328 Differential Revision: https://secure.phabricator.com/D20617 --- .../workflow/PhabricatorStorageManagementDumpWorkflow.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index c3b9a32327..28b188a873 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -285,10 +285,13 @@ final class PhabricatorStorageManagementDumpWorkflow $preamble = implode('', $preamble); $this->writeData($preamble, $file, $is_compress, $output_file); - $future = new ExecFuture('%C', $spec['command']); + // See T13328. The "mysql" command may produce output very quickly. + // Don't buffer more than a fixed amount. + $future = id(new ExecFuture('%C', $spec['command'])) + ->setReadBufferSize(32 * 1024 * 1024); $iterator = id(new FutureIterator(array($future))) - ->setUpdateInterval(0.100); + ->setUpdateInterval(0.010); foreach ($iterator as $ready) { list($stdout, $stderr) = $future->read(); $future->discardBuffers(); From 987e10461056b04bd5c9c4d7b9b5b6af8c6ce4b6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 25 Jun 2019 12:53:20 -0700 Subject: [PATCH 014/122] Bump the remarkup cache version after JIRA/Asana rule changes Summary: See PHI1319. Ref T13291. Bump the remarkup cache version, since the old JIRA / Asana rules may exist in the partial cached representation of remarkup blocks from older versions. Test Plan: Typed some comments with various formatting, saw remarkup work fine. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291 Differential Revision: https://secure.phabricator.com/D20619 --- src/infrastructure/markup/PhabricatorMarkupEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 3a63cb97a6..b9467e549d 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -42,7 +42,7 @@ final class PhabricatorMarkupEngine extends Phobject { private $objects = array(); private $viewer; private $contextObject; - private $version = 18; + private $version = 19; private $engineCaches = array(); private $auxiliaryConfig = array(); From 159fd44203192c4242f4435c28908f489c5b57e1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 26 Jun 2019 06:57:09 -0700 Subject: [PATCH 015/122] Correct transaction strengths after inconsitent scaling by 100 vs 1000 Summary: See D20540. I mistakenly multiplied some strenghts by 100 and others by 1000 when converting them to integers for `PhutilSortVector`. Multiply them all by 100 (that is, divide the ones which were multiplied by 1000 by 10) to put things back the way they were. Test Plan: quick mafs Reviewers: amckinley, richardvanvelzen Reviewed By: richardvanvelzen Differential Revision: https://secure.phabricator.com/D20622 --- .../storage/PhabricatorApplicationTransaction.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 775d59d4db..7c327393f8 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1375,16 +1375,16 @@ abstract class PhabricatorApplicationTransaction public function getActionStrength() { if ($this->isInlineCommentTransaction()) { - return 250; + return 25; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: - return 500; + return 50; case PhabricatorTransactions::TYPE_SUBSCRIBERS: if ($this->isSelfSubscription()) { // Make this weaker than TYPE_COMMENT. - return 250; + return 25; } if ($this->isApplicationAuthor()) { @@ -1396,14 +1396,14 @@ abstract class PhabricatorApplicationTransaction // In other cases, subscriptions are more interesting than comments // (which are shown anyway) but less interesting than any other type of // transaction. - return 750; + return 75; case PhabricatorTransactions::TYPE_MFA: // We want MFA signatures to render at the top of transaction groups, // on top of the things they signed. - return 10000; + return 1000; } - return 1000; + return 100; } public function isCommentTransaction() { From d6dc5d8e68bf9808755f7d52b664d2a8b0bf2121 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 28 Jun 2019 08:40:10 -0700 Subject: [PATCH 016/122] Fix the "x" link in tokenizer tokens incorrectly closing dialogs Summary: See . My Javascript is rusty: `'' + null == 'null'`. Same for `undefined`. Use an explicit typecheck instead. Test Plan: Clicked the "x" in a tokenizer token in a dialog, saw the token removed instead of the dialog closed. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20623 --- resources/celerity/map.php | 28 +++++++++---------- .../rsrc/externals/javelin/lib/Workflow.js | 6 +++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3708f83d02..adc19febba 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' => '8225dc58', + 'core.pkg.js' => '5a792749', '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' => '851f642d', + 'rsrc/externals/javelin/lib/Workflow.js' => '445e21a8', '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' => '851f642d', + 'javelin-workflow' => '445e21a8', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', 'maniphest-task-summary-css' => '61d1667e', @@ -1294,6 +1294,17 @@ 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', + ), '46116c01' => array( 'javelin-request', 'javelin-behavior', @@ -1607,17 +1618,6 @@ return array( 'javelin-resource', 'javelin-routable', ), - '851f642d' => array( - 'javelin-stratcom', - 'javelin-request', - 'javelin-dom', - 'javelin-vector', - 'javelin-install', - 'javelin-util', - 'javelin-mask', - 'javelin-uri', - 'javelin-routable', - ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js index d398d33774..3e1bba4a6a 100644 --- a/webroot/rsrc/externals/javelin/lib/Workflow.js +++ b/webroot/rsrc/externals/javelin/lib/Workflow.js @@ -104,7 +104,11 @@ JX.install('Workflow', { var link = event.getNode('tag:a'); // If the link is an anchor, or does not go anywhere, ignore the event. - var href = '' + link.getAttribute('href'); + var href = link.getAttribute('href'); + if (typeof href !== 'string') { + return; + } + if (!href.length || href[0] === '#') { return; } From 0e2cb6e7c437d76b97ca37e554f40637228439d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 29 Jun 2019 08:25:58 -0700 Subject: [PATCH 017/122] 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 018/122] 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 019/122] 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 020/122] 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 021/122] 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 022/122] 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 023/122] 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 024/122] 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 025/122] 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 026/122] 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 027/122] 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 028/122] 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 029/122] 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 030/122] 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 031/122] 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 032/122] 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 033/122] 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 034/122] 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 035/122] 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 036/122] 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 037/122] 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 038/122] 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 039/122] 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 040/122] 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 041/122] 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 042/122] 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 043/122] 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 044/122] 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 045/122] 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 046/122] 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 047/122] 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 048/122] 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 049/122] 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 050/122] 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 051/122] 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 052/122] 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 053/122] 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 054/122] 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 055/122] 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 056/122] 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 057/122] 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 058/122] 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 059/122] 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 060/122] 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 061/122] 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 062/122] 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 063/122] 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 064/122] 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 065/122] 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 066/122] 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 067/122] 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 068/122] 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 069/122] 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 070/122] 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(); From 76cd181bf379ef1650613b446f2a2c95f4746063 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 12:40:53 -0700 Subject: [PATCH 071/122] Don't try to emit project board update events if there are no projects to update Summary: Ref T4900. We may execute a bad query here if the task has no projects at all. Test Plan: Edited a task with no new or old projects. Instead of an exception, things worked. Maniphest Tasks: T4900 Differential Revision: https://secure.phabricator.com/D20689 --- .../editor/ManiphestTransactionEditor.php | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 247f5ce1fe..ed98ad8ad8 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -879,34 +879,36 @@ final class ManiphestTransactionEditor $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(); + if ($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; + $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]); + foreach ($notify_projects as $key => $project) { + if (!$project->getHasWorkboard()) { + unset($notify_projects[$key]); + } } - } - $notify_phids = array_keys($notify_projects); + $notify_phids = array_keys($notify_projects); - if ($notify_phids) { - $data = array( - 'type' => 'workboards', - 'subscribers' => $notify_phids, - ); + if ($notify_phids) { + $data = array( + 'type' => 'workboards', + 'subscribers' => $notify_phids, + ); - PhabricatorNotificationClient::tryToPostMessage($data); + PhabricatorNotificationClient::tryToPostMessage($data); + } } } From 8e263a2f6482203322de9ee5c0ec5664afa260c5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 12:40:56 -0700 Subject: [PATCH 072/122] Support "date" custom fields in "*.edit" endpoints Summary: Fixes T13355. This didn't appear to be a ton of extra work, we just didn't get it for free in the original implementation in D14635. Test Plan: - Saw "date" custom fields appear in Conduit API documentation for "maniphest.edit". - Set custom "date" field to null and non-null values via the API. {F6666582} Maniphest Tasks: T13355 Differential Revision: https://secure.phabricator.com/D20690 --- .../parametertype/ConduitEpochParameterType.php | 16 ++++++++++++++++ .../editfield/PhabricatorEpochEditField.php | 3 ++- .../PhabricatorStandardCustomFieldDate.php | 10 ++-------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/applications/conduit/parametertype/ConduitEpochParameterType.php b/src/applications/conduit/parametertype/ConduitEpochParameterType.php index e8fe095c50..8f2ca2c98a 100644 --- a/src/applications/conduit/parametertype/ConduitEpochParameterType.php +++ b/src/applications/conduit/parametertype/ConduitEpochParameterType.php @@ -3,8 +3,24 @@ final class ConduitEpochParameterType extends ConduitParameterType { + private $allowNull; + + public function setAllowNull($allow_null) { + $this->allowNull = $allow_null; + return $this; + } + + public function getAllowNull() { + return $this->allowNull; + } + protected function getParameterValue(array $request, $key, $strict) { $value = parent::getParameterValue($request, $key, $strict); + + if ($this->allowNull && ($value === null)) { + return $value; + } + $value = $this->parseIntValue($request, $key, $value, $strict); if ($value <= 0) { diff --git a/src/applications/transactions/editfield/PhabricatorEpochEditField.php b/src/applications/transactions/editfield/PhabricatorEpochEditField.php index 9ac9726593..b50f013177 100644 --- a/src/applications/transactions/editfield/PhabricatorEpochEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEpochEditField.php @@ -37,7 +37,8 @@ final class PhabricatorEpochEditField } protected function newConduitParameterType() { - return new ConduitEpochParameterType(); + return id(new ConduitEpochParameterType()) + ->setAllowNull($this->getAllowNull()); } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php index 994bb99403..4aba7543e7 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php @@ -226,20 +226,14 @@ final class PhabricatorStandardCustomFieldDate } } - - public function shouldAppearInConduitTransactions() { - // TODO: Dates are complicated and we don't yet support handling them from - // Conduit. - return false; - } - protected function newConduitSearchParameterType() { // TODO: Build a new "pair" type or similar. return null; } protected function newConduitEditParameterType() { - return new ConduitEpochParameterType(); + return id(new ConduitEpochParameterType()) + ->setAllowNull(!$this->getRequired()); } protected function newExportFieldType() { From b81c8380fb9753287bcf7c2b79eb064e55ea0bbf Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 31 Jul 2019 13:00:57 -0700 Subject: [PATCH 073/122] Document support for "limit" in tokenizer-based Custom Fields Summary: Fixes T13356. This option is supported and works fine, it just isn't documented. Add documentation and fix the config option to actually link to it to make life a little easier. Test Plan: Read documentation. Maniphest Tasks: T13356 Differential Revision: https://secure.phabricator.com/D20691 --- .../config/PhabricatorManiphestConfigOptions.php | 15 ++++++++++----- src/docs/user/configuration/custom_fields.diviner | 4 ++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index f1916cffec..077fc511ce 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -451,15 +451,20 @@ You can choose the default priority for newly created tasks with EOTEXT )); + $fields_description = $this->deformat(pht(<<newOption('maniphest.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Maniphest fields.')) - ->setDescription( - pht( - 'Array of custom fields for Maniphest tasks. For details on '. - 'adding custom fields to Maniphest, see "Configuring Custom '. - 'Fields" in the documentation.')) + ->setDescription($fields_description) ->addExample($fields_json, pht('Valid setting')), $this->newOption('maniphest.fields', $custom_field_type, $default_fields) ->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass()) diff --git a/src/docs/user/configuration/custom_fields.diviner b/src/docs/user/configuration/custom_fields.diviner index ecb7382648..75d83fc8ba 100644 --- a/src/docs/user/configuration/custom_fields.diviner +++ b/src/docs/user/configuration/custom_fields.diviner @@ -121,6 +121,10 @@ When defining custom fields using a configuration option like supported in text, int and remarkup fields (optional). - **copy**: If true, this field's value will be copied when an object is created using another object as a template. + - **limit**: For control types which use a tokenizer control to let the user + select a list of values, this limits how many values can be selected. For + example, a "users" field with a limit of "1" will behave like the "Owner" + field in Maniphest and only allow selection of a single user. The `strings` value supports different strings per control type. They are: From f5c380bfc94cd195a04e77d3864968a0fd2ae656 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 1 Aug 2019 10:21:43 -0700 Subject: [PATCH 074/122] Add very basic support for generating PDF documents Summary: Ref T13358. This is very minimal, but technically works. The eventual goal is to generate PDF invoices to make my life easier when I have to interact with Enterprise Vendor Procurement. Test Plan: {F6672439} Maniphest Tasks: T13358 Differential Revision: https://secure.phabricator.com/D20692 --- src/__phutil_library_map__.php | 31 ++++++ .../pdf/PhabricatorPDFCatalogObject.php | 26 +++++ .../pdf/PhabricatorPDFContentsObject.php | 25 +++++ .../phortune/pdf/PhabricatorPDFFontObject.php | 14 +++ .../phortune/pdf/PhabricatorPDFFragment.php | 38 +++++++ .../pdf/PhabricatorPDFFragmentOffset.php | 27 +++++ .../phortune/pdf/PhabricatorPDFGenerator.php | 59 ++++++++++ .../pdf/PhabricatorPDFHeadFragment.php | 10 ++ .../phortune/pdf/PhabricatorPDFInfoObject.php | 11 ++ .../phortune/pdf/PhabricatorPDFIterator.php | 103 ++++++++++++++++++ .../phortune/pdf/PhabricatorPDFObject.php | 95 ++++++++++++++++ .../phortune/pdf/PhabricatorPDFPageObject.php | 48 ++++++++ .../pdf/PhabricatorPDFPagesObject.php | 38 +++++++ .../pdf/PhabricatorPDFResourcesObject.php | 28 +++++ .../pdf/PhabricatorPDFTailFragment.php | 72 ++++++++++++ 15 files changed, 625 insertions(+) create mode 100644 src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFContentsObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFFontObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFFragment.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFGenerator.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFInfoObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFIterator.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFPageObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFPagesObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php create mode 100644 src/applications/phortune/pdf/PhabricatorPDFTailFragment.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5abfd1bc4d..03e0163ec8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3878,7 +3878,21 @@ phutil_register_library_map(array( 'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php', 'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php', 'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php', + 'PhabricatorPDFCatalogObject' => 'applications/phortune/pdf/PhabricatorPDFCatalogObject.php', + 'PhabricatorPDFContentsObject' => 'applications/phortune/pdf/PhabricatorPDFContentsObject.php', 'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php', + 'PhabricatorPDFFontObject' => 'applications/phortune/pdf/PhabricatorPDFFontObject.php', + 'PhabricatorPDFFragment' => 'applications/phortune/pdf/PhabricatorPDFFragment.php', + 'PhabricatorPDFFragmentOffset' => 'applications/phortune/pdf/PhabricatorPDFFragmentOffset.php', + 'PhabricatorPDFGenerator' => 'applications/phortune/pdf/PhabricatorPDFGenerator.php', + 'PhabricatorPDFHeadFragment' => 'applications/phortune/pdf/PhabricatorPDFHeadFragment.php', + 'PhabricatorPDFInfoObject' => 'applications/phortune/pdf/PhabricatorPDFInfoObject.php', + 'PhabricatorPDFIterator' => 'applications/phortune/pdf/PhabricatorPDFIterator.php', + 'PhabricatorPDFObject' => 'applications/phortune/pdf/PhabricatorPDFObject.php', + 'PhabricatorPDFPageObject' => 'applications/phortune/pdf/PhabricatorPDFPageObject.php', + 'PhabricatorPDFPagesObject' => 'applications/phortune/pdf/PhabricatorPDFPagesObject.php', + 'PhabricatorPDFResourcesObject' => 'applications/phortune/pdf/PhabricatorPDFResourcesObject.php', + 'PhabricatorPDFTailFragment' => 'applications/phortune/pdf/PhabricatorPDFTailFragment.php', 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', @@ -10101,7 +10115,24 @@ phutil_register_library_map(array( 'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField', + 'PhabricatorPDFCatalogObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFContentsObject' => 'PhabricatorPDFObject', 'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine', + 'PhabricatorPDFFontObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFFragment' => 'Phobject', + 'PhabricatorPDFFragmentOffset' => 'Phobject', + 'PhabricatorPDFGenerator' => 'Phobject', + 'PhabricatorPDFHeadFragment' => 'PhabricatorPDFFragment', + 'PhabricatorPDFInfoObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFIterator' => array( + 'Phobject', + 'Iterator', + ), + 'PhabricatorPDFObject' => 'PhabricatorPDFFragment', + 'PhabricatorPDFPageObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFPagesObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFResourcesObject' => 'PhabricatorPDFObject', + 'PhabricatorPDFTailFragment' => 'PhabricatorPDFFragment', 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', diff --git a/src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php b/src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php new file mode 100644 index 0000000000..9cf4d2324e --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFCatalogObject.php @@ -0,0 +1,26 @@ +pagesObject = $this->newChildObject($pages_object); + return $this; + } + + public function getPagesObject() { + return $this->pagesObject; + } + + protected function writeObject() { + $this->writeLine('/Type /Catalog'); + + $pages_object = $this->getPagesObject(); + if ($pages_object) { + $this->writeLine('/Pages %d 0 R', $pages_object->getObjectIndex()); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php b/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php new file mode 100644 index 0000000000..f49a32df33 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFContentsObject.php @@ -0,0 +1,25 @@ +rawContent = $raw_content; + return $this; + } + + public function getRawContent() { + return $this->rawContent; + } + + protected function writeObject() { + $data = $this->getRawContent(); + + $stream_length = $this->newStream($data); + + $this->writeLine('/Filter /FlateDecode /Length %d', $stream_length); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFontObject.php b/src/applications/phortune/pdf/PhabricatorPDFFontObject.php new file mode 100644 index 0000000000..71f128d3a5 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFontObject.php @@ -0,0 +1,14 @@ +writeLine('/Type /Font'); + + $this->writeLine('/BaseFont /Helvetica-Bold'); + $this->writeLine('/Subtype /Type1'); + $this->writeLine('/Encoding /WinAnsiEncoding'); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFragment.php b/src/applications/phortune/pdf/PhabricatorPDFFragment.php new file mode 100644 index 0000000000..eb113b6140 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFragment.php @@ -0,0 +1,38 @@ +rope = new PhutilRope(); + + $this->writeFragment(); + + $rope = $this->rope; + $this->rope = null; + + return $rope->getAsString(); + } + + public function hasRefTableEntry() { + return false; + } + + abstract protected function writeFragment(); + + final protected function writeLine($pattern) { + $pattern = $pattern."\n"; + + $argv = func_get_args(); + $argv[0] = $pattern; + + $line = call_user_func_array('sprintf', $argv); + + $this->rope->append($line); + + return $this; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php b/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php new file mode 100644 index 0000000000..c8b2769d7a --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFFragmentOffset.php @@ -0,0 +1,27 @@ +fragment = $fragment; + return $this; + } + + public function getFragment() { + return $this->fragment; + } + + public function setOffset($offset) { + $this->offset = $offset; + return $this; + } + + public function getOffset() { + return $this->offset; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFGenerator.php b/src/applications/phortune/pdf/PhabricatorPDFGenerator.php new file mode 100644 index 0000000000..f2c5c92359 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFGenerator.php @@ -0,0 +1,59 @@ +hasIterator) { + throw new Exception( + pht( + 'This generator has already emitted an iterator. You can not '. + 'modify the PDF document after you begin writing it.')); + } + + $this->objects[] = $object; + $index = count($this->objects); + + $object->setGenerator($this, $index); + + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function newIterator() { + $this->hasIterator = true; + return id(new PhabricatorPDFIterator()) + ->setGenerator($this); + } + + public function setInfoObject(PhabricatorPDFInfoObject $info_object) { + $this->addObject($info_object); + $this->infoObject = $info_object; + return $this; + } + + public function getInfoObject() { + return $this->infoObject; + } + + public function setCatalogObject( + PhabricatorPDFCatalogObject $catalog_object) { + $this->addObject($catalog_object); + $this->catalogObject = $catalog_object; + return $this; + } + + public function getCatalogObject() { + return $this->catalogObject; + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php b/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php new file mode 100644 index 0000000000..ef12bacce9 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFHeadFragment.php @@ -0,0 +1,10 @@ +writeLine('%s', '%PDF-1.3'); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php b/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php new file mode 100644 index 0000000000..2aba63c407 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFInfoObject.php @@ -0,0 +1,11 @@ +writeLine('/Producer (Phabricator 20190801)'); + $this->writeLine('/CreationDate (D:%s)', date('YmdHis')); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFIterator.php b/src/applications/phortune/pdf/PhabricatorPDFIterator.php new file mode 100644 index 0000000000..d39168369d --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFIterator.php @@ -0,0 +1,103 @@ +generator) { + throw new Exception( + pht( + 'This iterator already has a generator. You can not modify the '. + 'generator for a given iterator.')); + } + + $this->generator = $generator; + + return $this; + } + + public function getGenerator() { + if (!$this->generator) { + throw new Exception( + pht( + 'This PDF iterator has no associated PDF generator.')); + } + + return $this->generator; + } + + public function getFragmentOffsets() { + return $this->fragmentOffsets; + } + + public function current() { + return $this->fragmentBytes; + } + + public function key() { + return $this->framgentKey; + } + + public function next() { + $this->fragmentKey++; + + if (!$this->valid()) { + return; + } + + $fragment = $this->fragments[$this->fragmentKey]; + + $this->fragmentOffsets[] = id(new PhabricatorPDFFragmentOffset()) + ->setFragment($fragment) + ->setOffset($this->byteLength); + + $bytes = $fragment->getAsBytes(); + + $this->fragmentBytes = $bytes; + $this->byteLength += strlen($bytes); + } + + public function rewind() { + if ($this->hasRewound) { + throw new Exception( + pht( + 'PDF iterators may not be rewound. Create a new iterator to emit '. + 'another PDF.')); + } + + $generator = $this->getGenerator(); + $objects = $generator->getObjects(); + + $this->fragments = array(); + $this->fragments[] = new PhabricatorPDFHeadFragment(); + + foreach ($objects as $object) { + $this->fragments[] = $object; + } + + $this->fragments[] = id(new PhabricatorPDFTailFragment()) + ->setIterator($this); + + $this->hasRewound = true; + + $this->fragmentKey = -1; + $this->byteLength = 0; + + $this->next(); + } + + public function valid() { + return isset($this->fragments[$this->fragmentKey]); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFObject.php b/src/applications/phortune/pdf/PhabricatorPDFObject.php new file mode 100644 index 0000000000..49c14d2f00 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFObject.php @@ -0,0 +1,95 @@ +writeLine('%d 0 obj', $this->getObjectIndex()); + $this->writeLine('<<'); + $this->writeObject(); + $this->writeLine('>>'); + + $streams = $this->streams; + $this->streams = array(); + foreach ($streams as $stream) { + $this->writeLine('stream'); + $this->writeLine('%s', $stream); + $this->writeLine('endstream'); + } + + $this->writeLine('endobj'); + } + + final public function setGenerator( + PhabricatorPDFGenerator $generator, + $index) { + + if ($this->getGenerator()) { + throw new Exception( + pht( + 'This PDF object is already registered with a PDF generator. You '. + 'can not register an object with more than one generator.')); + } + + $this->generator = $generator; + $this->objectIndex = $index; + + foreach ($this->getChildren() as $child) { + $generator->addObject($child); + } + + return $this; + } + + final public function getGenerator() { + return $this->generator; + } + + final public function getObjectIndex() { + if (!$this->objectIndex) { + throw new Exception( + pht( + 'Trying to get index for object ("%s") which has not been '. + 'registered with a generator.', + get_class($this))); + } + + return $this->objectIndex; + } + + final protected function newChildObject(PhabricatorPDFObject $object) { + if ($this->generator) { + throw new Exception( + pht( + 'Trying to add a new PDF Object child after already registering '. + 'the object with a generator.')); + } + + $this->children[] = $object; + return $object; + } + + private function getChildren() { + return $this->children; + } + + abstract protected function writeObject(); + + final protected function newStream($raw_data) { + $stream_data = gzcompress($raw_data); + + $this->streams[] = $stream_data; + + return strlen($stream_data); + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFPageObject.php b/src/applications/phortune/pdf/PhabricatorPDFPageObject.php new file mode 100644 index 0000000000..3137d45d12 --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFPageObject.php @@ -0,0 +1,48 @@ +pagesObject = $pages; + return $this; + } + + public function setContentsObject(PhabricatorPDFContentsObject $contents) { + $this->contentsObject = $this->newChildObject($contents); + return $this; + } + + public function setResourcesObject(PhabricatorPDFResourcesObject $resources) { + $this->resourcesObject = $this->newChildObject($resources); + return $this; + } + + protected function writeObject() { + $this->writeLine('/Type /Page'); + + $pages_object = $this->pagesObject; + $contents_object = $this->contentsObject; + $resources_object = $this->resourcesObject; + + if ($pages_object) { + $pages_index = $pages_object->getObjectIndex(); + $this->writeLine('/Parent %d 0 R', $pages_index); + } + + if ($contents_object) { + $contents_index = $contents_object->getObjectIndex(); + $this->writeLine('/Contents %d 0 R', $contents_index); + } + + if ($resources_object) { + $resources_index = $resources_object->getObjectIndex(); + $this->writeLine('/Resources %d 0 R', $resources_index); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php b/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php new file mode 100644 index 0000000000..4f0b89886e --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFPagesObject.php @@ -0,0 +1,38 @@ +setPagesObject($this); + $this->pageObjects[] = $this->newChildObject($page); + return $this; + } + + public function getPageObjects() { + return $this->pageObjects; + } + + protected function writeObject() { + $this->writeLine('/Type /Pages'); + + $page_objects = $this->getPageObjects(); + + $this->writeLine('/Count %d', count($page_objects)); + $this->writeLine('/MediaBox [%d %d %0.2f %0.2f]', 0, 0, 595.28, 841.89); + + if ($page_objects) { + $kids = array(); + foreach ($page_objects as $page_object) { + $kids[] = sprintf( + '%d 0 R', + $page_object->getObjectIndex()); + } + + $this->writeLine('/Kids [%s]', implode(' ', $kids)); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php b/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php new file mode 100644 index 0000000000..9414708f6d --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFResourcesObject.php @@ -0,0 +1,28 @@ +fontObjects[] = $this->newChildObject($font); + return $this; + } + + public function getFontObjects() { + return $this->fontObjects; + } + + protected function writeObject() { + $this->writeLine('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + + $fonts = $this->getFontObjects(); + foreach ($fonts as $font) { + $this->writeLine('/Font <<'); + $this->writeLine('/F%d %d 0 R', 1, $font->getObjectIndex()); + $this->writeLine('>>'); + } + } + +} diff --git a/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php b/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php new file mode 100644 index 0000000000..2f606a1c8c --- /dev/null +++ b/src/applications/phortune/pdf/PhabricatorPDFTailFragment.php @@ -0,0 +1,72 @@ +iterator = $iterator; + return $this; + } + + public function getIterator() { + return $this->iterator; + } + + protected function writeFragment() { + $iterator = $this->getIterator(); + $generator = $iterator->getGenerator(); + $objects = $generator->getObjects(); + + $xref_offset = null; + + $this->writeLine('xref'); + $this->writeLine('0 %d', count($objects) + 1); + $this->writeLine('%010d %05d f ', 0, 0xFFFF); + + $offset_map = array(); + + $fragment_offsets = $iterator->getFragmentOffsets(); + foreach ($fragment_offsets as $fragment_offset) { + $fragment = $fragment_offset->getFragment(); + $offset = $fragment_offset->getOffset(); + + if ($fragment === $this) { + $xref_offset = $offset; + } + + if (!$fragment->hasRefTableEntry()) { + continue; + } + + $offset_map[$fragment->getObjectIndex()] = $offset; + } + + ksort($offset_map); + + foreach ($offset_map as $offset) { + $this->writeLine('%010d %05d n ', $offset, 0); + } + + $this->writeLine('trailer'); + $this->writeLine('<<'); + $this->writeLine('/Size %d', count($objects) + 1); + + $info_object = $generator->getInfoObject(); + if ($info_object) { + $this->writeLine('/Info %d 0 R', $info_object->getObjectIndex()); + } + + $catalog_object = $generator->getCatalogObject(); + if ($catalog_object) { + $this->writeLine('/Root %d 0 R', $catalog_object->getObjectIndex()); + } + + $this->writeLine('>>'); + $this->writeLine('startxref'); + $this->writeLine('%d', $xref_offset); + $this->writeLine('%s', '%%EOF'); + } + +} From 3069ef41662d916ea57d254b10491d6e852d8d46 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 1 Aug 2019 12:02:01 -0700 Subject: [PATCH 075/122] Prevent object titles in the "Object Attacher" dialog from triggering Quicksand "Close Dialog on Navigation" behavior Summary: Fixes T13363. Currently, these are genuine links which we intercept events for. Make them pseudolinks instead. Possible alternative approaches are: - Keep them as genuine links, but mark them as non-navigation links for Quicksand. (But: yuck, weird special case.) - Keep them as genuine links, and have the dialog handler `JX.Stratcom.pass()` to see if anything handles the event. (But: the "pass()" pattern generally feels bad.) "Tableaus" or whatever comes out of T10469 some day will probably break everything anyway? Test Plan: - Opened the "Edit Related Tasks... > Edit Subtasks" dialog. - Clicked task title links (not the "open in new window" icon, and not the "Select" button). - Before: Dialog (sometimes) closed abruptly. - After: Task is consistently selected as part of the attachment set. Maniphest Tasks: T13363 Differential Revision: https://secure.phabricator.com/D20693 --- resources/celerity/map.php | 18 +++++++++--------- .../rsrc/js/core/behavior-object-selector.js | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ca09a081e5..9dc1d68fd9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -12,7 +12,7 @@ return array( 'core.pkg.css' => 'af983028', 'core.pkg.js' => '73a06a9f', 'differential.pkg.css' => '8d8360fb', - 'differential.pkg.js' => '67e02996', + 'differential.pkg.js' => '0b037a4f', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => 'a98c0bf7', 'maniphest.pkg.css' => '35995d6d', @@ -484,7 +484,7 @@ return array( 'rsrc/js/core/behavior-line-linker.js' => 'e15c8b1f', 'rsrc/js/core/behavior-linked-container.js' => '74446546', 'rsrc/js/core/behavior-more.js' => '506aa3f4', - 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a', + 'rsrc/js/core/behavior-object-selector.js' => '98ef467f', 'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22', 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f', @@ -645,7 +645,7 @@ return array( 'javelin-behavior-phabricator-line-linker' => 'e15c8b1f', 'javelin-behavior-phabricator-nav' => 'f166c949', 'javelin-behavior-phabricator-notification-example' => '29819b75', - 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a', + 'javelin-behavior-phabricator-object-selector' => '98ef467f', 'javelin-behavior-phabricator-oncopy' => 'ff7b3f22', 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f', 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', @@ -1730,6 +1730,12 @@ return array( 'javelin-dom', 'javelin-router', ), + '98ef467f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-request', + 'javelin-util', + ), '9aae2b66' => array( 'javelin-install', 'javelin-util', @@ -1790,12 +1796,6 @@ return array( 'phui-button-css', 'phui-button-simple-css', ), - 'a4af0b4a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-request', - 'javelin-util', - ), 'a5257c4e' => array( 'javelin-install', 'javelin-dom', diff --git a/webroot/rsrc/js/core/behavior-object-selector.js b/webroot/rsrc/js/core/behavior-object-selector.js index 722cfdd562..b28dac0926 100644 --- a/webroot/rsrc/js/core/behavior-object-selector.js +++ b/webroot/rsrc/js/core/behavior-object-selector.js @@ -132,7 +132,7 @@ JX.behavior('phabricator-object-selector', function(config) { var select_object_link = JX.$N( 'a', - {href: h.uri, sigil: 'object-attacher'}, + {href: '#', sigil: 'object-attacher'}, h.name); var select_object_button = JX.$N( From 1fe631116771b834a8e25e801d08bc3b0e9945e5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 2 Aug 2019 09:15:06 -0700 Subject: [PATCH 076/122] Modernize user and repository "delete" workflows and improve documentation Summary: Fixes T8830. Fixes T13364. - The inability to destroy objects from the web UI is intentional. Make this clear in the messaging, which is somewhat out of date and partly reflects an earlier era when things could be destroyed. - `bin/remove destroy` can't rewind time. Document expectations around the "put the cat back in the bag" use case. Test Plan: Read documentation, clicked through both workflows. Maniphest Tasks: T13364, T8830 Differential Revision: https://secure.phabricator.com/D20694 --- resources/celerity/map.php | 6 +- ...iffusionRepositoryEditDeleteController.php | 47 +++++----- ...ffusionRepositoryBasicsManagementPanel.php | 2 - .../PhabricatorPeopleDeleteController.php | 75 ++++++--------- .../field/permanently_destroying_data.diviner | 92 +++++++++++++++++++ .../user/userguide/diffusion_managing.diviner | 9 +- src/view/AphrontDialogView.php | 35 +++++-- webroot/rsrc/css/aphront/dialog-view.css | 5 + 8 files changed, 181 insertions(+), 90 deletions(-) create mode 100644 src/docs/user/field/permanently_destroying_data.diviner diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9dc1d68fd9..4eeb433e96 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'af983028', + 'core.pkg.css' => '5a4a5010', 'core.pkg.js' => '73a06a9f', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '0b037a4f', @@ -24,7 +24,7 @@ return array( 'rsrc/audio/basic/ting.mp3' => 'a6b6540e', 'rsrc/css/aphront/aphront-bars.css' => '4a327b4a', 'rsrc/css/aphront/dark-console.css' => '7f06cda2', - 'rsrc/css/aphront/dialog-view.css' => 'b70c70df', + 'rsrc/css/aphront/dialog-view.css' => '874f5c06', 'rsrc/css/aphront/list-filter-view.css' => 'feb64255', 'rsrc/css/aphront/multi-column.css' => 'fbc00ba3', 'rsrc/css/aphront/notification.css' => '30240bd2', @@ -530,7 +530,7 @@ return array( 'almanac-css' => '2e050f4f', 'aphront-bars' => '4a327b4a', 'aphront-dark-console-css' => '7f06cda2', - 'aphront-dialog-view-css' => 'b70c70df', + 'aphront-dialog-view-css' => '874f5c06', 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php index 0380b99f0f..93fe70c0fe 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php @@ -17,32 +17,31 @@ final class DiffusionRepositoryEditDeleteController ->setRepository($repository) ->getPanelURI(); - $dialog = new AphrontDialogView(); - $text_1 = pht( - 'If you really want to delete the repository, run this command from '. - 'the command line:'); - $command = csprintf( - 'phabricator/ $ ./bin/remove destroy %R', - $repository->getMonogram()); - $text_2 = pht( - 'Repositories touch many objects and as such deletes are '. - 'prohibitively expensive to run from the web UI.'); - $body = phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - array( - phutil_tag('p', array(), $text_1), - phutil_tag('p', array(), - phutil_tag('tt', array(), $command)), - phutil_tag('p', array(), $text_2), - )); + $doc_uri = PhabricatorEnv::getDoclink( + 'Permanently Destroying Data'); return $this->newDialog() - ->setTitle(pht('Really want to delete the repository?')) - ->appendChild($body) - ->addCancelButton($panel_uri, pht('Okay')); + ->setTitle(pht('Delete Repository')) + ->appendParagraph( + pht( + 'To permanently destroy this repository, run this command from '. + 'the command line:')) + ->appendCommand( + csprintf( + 'phabricator/ $ ./bin/remove destroy %R', + $repository->getMonogram())) + ->appendParagraph( + pht( + 'Repositories can not be permanently destroyed from the web '. + 'interface. See %s in the documentation for more information.', + phutil_tag( + 'a', + array( + 'href' => $doc_uri, + 'target' => '_blank', + ), + pht('Permanently Destroying Data')))) + ->addCancelButton($panel_uri, pht('Close')); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php index 2d006136e3..02df82c7c0 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -155,8 +155,6 @@ final class DiffusionRepositoryBasicsManagementPanel ->setName(pht('Delete Repository')) ->setHref($delete_uri) ->setIcon('fa-times') - ->setColor(PhabricatorActionView::RED) - ->setDisabled(true) ->setWorkflow(true)); return $this->newCurtainView() diff --git a/src/applications/people/controller/PhabricatorPeopleDeleteController.php b/src/applications/people/controller/PhabricatorPeopleDeleteController.php index 29ae7edd93..8e6ac91da7 100644 --- a/src/applications/people/controller/PhabricatorPeopleDeleteController.php +++ b/src/applications/people/controller/PhabricatorPeopleDeleteController.php @@ -17,58 +17,35 @@ final class PhabricatorPeopleDeleteController $manage_uri = $this->getApplicationURI("manage/{$id}/"); - if ($user->getPHID() == $viewer->getPHID()) { - return $this->buildDeleteSelfResponse($manage_uri); - } - - $str1 = pht( - 'Be careful when deleting users! This will permanently and '. - 'irreversibly destroy this user account.'); - - $str2 = pht( - 'If this user interacted with anything, it is generally better to '. - 'disable them, not delete them. If you delete them, it will no longer '. - 'be possible to (for example) search for objects they created, and you '. - 'will lose other information about their history. Disabling them '. - 'instead will prevent them from logging in, but will not destroy any of '. - 'their data.'); - - $str3 = pht( - 'It is generally safe to delete newly created users (and test users and '. - 'so on), but less safe to delete established users. If possible, '. - 'disable them instead.'); - - $str4 = pht('To permanently destroy this user, run this command:'); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendRemarkupInstructions( - csprintf( - " phabricator/ $ ./bin/remove destroy %R\n", - '@'.$user->getUsername())); + $doc_uri = PhabricatorEnv::getDoclink( + 'Permanently Destroying Data'); return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setTitle(pht('Permanently Delete User')) - ->setShortTitle(pht('Delete User')) - ->appendParagraph($str1) - ->appendParagraph($str2) - ->appendParagraph($str3) - ->appendParagraph($str4) - ->appendChild($form->buildLayoutView()) + ->setTitle(pht('Delete User')) + ->appendParagraph( + pht( + 'To permanently destroy this user, run this command from the '. + 'command line:')) + ->appendCommand( + csprintf( + 'phabricator/ $ ./bin/remove destroy %R', + $user->getMonogram())) + ->appendParagraph( + pht( + 'Unless you have a very good reason to delete this user, consider '. + 'disabling them instead.')) + ->appendParagraph( + pht( + 'Users can not be permanently destroyed from the web interface. '. + 'See %s in the documentation for more information.', + phutil_tag( + 'a', + array( + 'href' => $doc_uri, + 'target' => '_blank', + ), + pht('Permanently Destroying Data')))) ->addCancelButton($manage_uri, pht('Close')); } - private function buildDeleteSelfResponse($cancel_uri) { - return $this->newDialog() - ->setTitle(pht('You Shall Journey No Farther')) - ->appendParagraph( - pht( - 'As you stare into the gaping maw of the abyss, something '. - 'holds you back.')) - ->appendParagraph(pht('You can not delete your own account.')) - ->addCancelButton($cancel_uri, pht('Turn Back')); - } - - } diff --git a/src/docs/user/field/permanently_destroying_data.diviner b/src/docs/user/field/permanently_destroying_data.diviner new file mode 100644 index 0000000000..04907fc0be --- /dev/null +++ b/src/docs/user/field/permanently_destroying_data.diviner @@ -0,0 +1,92 @@ +@title Permanently Destroying Data +@group fieldmanual + +How to permanently destroy data and manage leaked secrets. + +Overview +======== + +Phabricator intentionally makes it difficult to permanently destroy data, but +provides a command-line tool for destroying objects if you're certain that +you want to destroy something. + +**Disable vs Destroy**: Most kinds of objects can be disabled, deactivated, +closed, or archived. These operations place them in inactive states and +preserve their transaction history. + +(NOTE) Disabling (rather than destroying) objects is strongly recommended +unless you have a very good reason to want to permanently destroy an object. + + +Destroying Data +=============== + +To permanently destroy an object, run this command from the command line: + +``` +phabricator/ $ ./bin/remove destroy +``` + +The `` may be an object monogram or PHID. For instance, you can use +`@alice` to destroy a particular user, or `T123` to destroy a particular task. + +(IMPORTANT) This operation is permanent and can not be undone. + + +CLI Access Required +=================== + +In almost all cases, Phabricator requires operational access from the CLI to +permanently destroy data. One major reason for this requirement is that it +limits the reach of an attacker who compromises a privileged account. + +The web UI is generally append-only and actions generally leave an audit +trail, usually in the transaction log. Thus, an attacker who compromises an +account but only gains access to the web UI usually can not do much permanent +damage and usually can not hide their actions or cover their tracks. + +Another reason that destroying data is hard is simply that it's permanent and +can not be undone, so there's no way to recover from mistakes. + + +Leaked Secrets +============== + +Sometimes you may want to destroy an object because it has leaked a secret, +like an API key or another credential. For example, an engineer might +accidentally send a change for review which includes a sensitive private key. + +No Phabricator command can rewind time, and once data is written to Phabricator +the cat is often out of the bag: it has often been transmitted to external +systems which Phabricator can not interact with via email, webhooks, API calls, +repository mirroring, CDN caching, and so on. You can try to clean up the mess, +but you're generally already too late. + +The `bin/remove destroy` command will make a reasonable attempt to completely +destroy objects, but this is just an attempt. It can not unsend email or uncall +the API, and no command can rewind time and undo a leak. + +**Revoking Credentials**: If Phabricator credentials were accidentally +disclosed, you can revoke them so they no longer function. See +@{article:Revoking Credentials} for more information. + + +Preventing Leaks +================ + +Because time can not be rewound, it is best to prevent sensitive data from +leaking in the first place. Phabricator supports some technical measures that +can make it more difficult to accidentally disclose secrets: + +**Differential Diff Herald Rules**: You can write "Differential Diff" rules +in Herald that reject diffs before they are written to disk by using the +"Block diff with message" action. + +These rules can reject diffs based on affected file names or file content. +This is a coarse tool, but rejecting diffs which contain strings like +`BEGIN RSA PRIVATE KEY` may make it more difficult to accidentally disclose +certain secrets. + +**Commit Content Herald Rules**: For hosted repositories, you can write +"Commit Hook: Commit Content" rules in Herald which reject pushes that contain +commit which match certain rules (like file name or file content rules). diff --git a/src/docs/user/userguide/diffusion_managing.diviner b/src/docs/user/userguide/diffusion_managing.diviner index aa52c1c475..138bc918bc 100644 --- a/src/docs/user/userguide/diffusion_managing.diviner +++ b/src/docs/user/userguide/diffusion_managing.diviner @@ -169,8 +169,8 @@ start working normally. Basics: Delete Repository ========================= -Repositories can not be deleted from the web UI, so this option is always -disabled. Clicking it gives you information about how to delete a repository. +Repositories can not be deleted from the web UI, so this option only gives you +information about how to delete a repository. Repositories can only be deleted from the command line, with `bin/remove`: @@ -178,9 +178,8 @@ Repositories can only be deleted from the command line, with `bin/remove`: $ ./bin/remove destroy ``` -WARNING: This command will issue you a dire warning about the severity of the -action you are taking. Heed this warning. You are **strongly discouraged** from -destroying repositories. Instead, deactivate them. +This command will permanently destroy the repository. For more information +about destroying things, see @{article:Permanently Destroying Data}. Policies diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 52681845d2..09fc8e7a16 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -161,15 +161,36 @@ final class AphrontDialogView } public function appendParagraph($paragraph) { - return $this->appendChild( - phutil_tag( - 'p', - array( - 'class' => 'aphront-dialog-view-paragraph', - ), - $paragraph)); + return $this->appendParagraphTag($paragraph); } + public function appendCommand($command) { + $command_tag = phutil_tag('tt', array(), $command); + return $this->appendParagraphTag( + $command_tag, + 'aphront-dialog-view-command'); + } + + private function appendParagraphTag($content, $classes = null) { + if ($classes) { + $classes = (array)$classes; + } else { + $classes = array(); + } + + array_unshift($classes, 'aphront-dialog-view-paragraph'); + + $paragraph_tag = phutil_tag( + 'p', + array( + 'class' => implode(' ', $classes), + ), + $content); + + return $this->appendChild($paragraph_tag); + } + + public function appendList(array $items) { $listitems = array(); foreach ($items as $item) { diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 153685548e..b47ca14850 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -158,6 +158,11 @@ margin-top: 16px; } +.aphront-dialog-view-command { + padding: 8px 16px; + background: {$greybackground}; +} + .device-desktop .aphront-dialog-flush .phui-oi-list-view { margin: 0; padding: 0; From 6c41508906778464cf60be5207821abe80ff57a3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 2 Aug 2019 09:38:13 -0700 Subject: [PATCH 077/122] Fix an issue where lines with more than one pattern match highlighted improperly in Diffusion Summary: Ref T13339. If a search pattern matches more than once on a line, we currently render the line incorreclty, duplicating some of the text. `substr()` is being called as though the third parameter was `end_offset`, but it's actually `length`. Correct the parameter. Test Plan: Before: {F6676625} After: {F6676623} Maniphest Tasks: T13339 Differential Revision: https://secure.phabricator.com/D20695 --- src/applications/diffusion/view/DiffusionPatternSearchView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/diffusion/view/DiffusionPatternSearchView.php b/src/applications/diffusion/view/DiffusionPatternSearchView.php index 93ef42d46d..39a1df4e89 100644 --- a/src/applications/diffusion/view/DiffusionPatternSearchView.php +++ b/src/applications/diffusion/view/DiffusionPatternSearchView.php @@ -47,7 +47,7 @@ final class DiffusionPatternSearchView extends DiffusionView { $offset = $match[1]; if ($cursor != $offset) { $output[] = array( - 'text' => substr($string, $cursor, $offset), + 'text' => substr($string, $cursor, ($offset - $cursor)), 'highlight' => false, ); } From 87f878ec8a774e3de0f4f596304bebf8f44c0d30 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 2 Aug 2019 10:44:24 -0700 Subject: [PATCH 078/122] Stop trying to CC merchants on invoices/receipts Summary: Fixes T13341. Currently, cart emails (invoices/receipts) are sent to members of the associated merchant account. This was just a simple way to keep an eye on things when this was first written. The system works fine, and recent changes (almost certainly D20525) stopped these emails from working (presumably because of the slightly weird merchant permissions model). This could be sorted out in more detail, but it looks like the path forward is to introduce a side channel for email anyway (via T8389), and that's a better way to implement this behavior since it means the normal recipients won't see a bunch of random staff/merchant email addresses on their receipts. Test Plan: Grepped for `merchant` in this editor. Maniphest Tasks: T13341 Differential Revision: https://secure.phabricator.com/D20696 --- src/applications/phortune/editor/PhortuneCartEditor.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/applications/phortune/editor/PhortuneCartEditor.php b/src/applications/phortune/editor/PhortuneCartEditor.php index dcf1f2d0e0..854c4ab9c7 100644 --- a/src/applications/phortune/editor/PhortuneCartEditor.php +++ b/src/applications/phortune/editor/PhortuneCartEditor.php @@ -188,8 +188,8 @@ final class PhortuneCartEditor protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); - // Reload the cart to pull merchant and account information, in case we - // just created the object. + // Reload the cart to pull account information, in case we just created the + // object. $cart = id(new PhortuneCartQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($object->getPHID())) @@ -199,10 +199,6 @@ final class PhortuneCartEditor $phids[] = $account_member; } - foreach ($cart->getMerchant()->getMemberPHIDs() as $merchant_member) { - $phids[] = $merchant_member; - } - return $phids; } From 31254c5124d5d91a020fd003c94e8d95d6afb266 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 7 Aug 2019 08:57:19 -0700 Subject: [PATCH 079/122] Correct column options presented in "Move tasks to project..." on workboards Summary: Ref T13368. The column options presented to the user are currently incorrect because the wrong set of columns are drawn from. Test Plan: On a workboard, used "Move tasks to project..." to target another board, saw that board's columns. Maniphest Tasks: T13368 Differential Revision: https://secure.phabricator.com/D20698 --- .../controller/PhabricatorProjectColumnBulkMoveController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php index 2b4c536c8d..7dc4c77e1c 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php @@ -107,7 +107,7 @@ final class PhabricatorProjectColumnBulkMoveController ->executeLayout(); $dst_columns = $layout_engine->getColumns($dst_project->getPHID()); - $dst_columns = mpull($columns, null, 'getPHID'); + $dst_columns = mpull($dst_columns, null, 'getPHID'); $has_column = false; $dst_column = null; From 6deac356599ffff00572ba52c6706a81337d76d3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 7 Aug 2019 08:59:34 -0700 Subject: [PATCH 080/122] Don't show proxy (subproject/milestone) columns as options in "Move tasks..." workflows from workboards Summary: Ref T13368. Proxy columns should not be selectable from this workflow. If you want to move tasks to milestone/subproject X, do "Move tasks to project..." and pick X as the project. (This could be made to work some day.) Test Plan: Went through a "Move tasks to project..." workflow targeting a project with subprojects. No longer saw subproject columns presented as dropdown options. Maniphest Tasks: T13368 Differential Revision: https://secure.phabricator.com/D20699 --- .../PhabricatorProjectColumnBulkMoveController.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php index 7dc4c77e1c..5792664110 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php @@ -109,6 +109,15 @@ final class PhabricatorProjectColumnBulkMoveController $dst_columns = $layout_engine->getColumns($dst_project->getPHID()); $dst_columns = mpull($dst_columns, null, 'getPHID'); + // Prevent moves to milestones or subprojects by selecting their + // columns, since the implications aren't obvious and this doesn't + // work the same way as normal column moves. + foreach ($dst_columns as $key => $dst_column) { + if ($dst_column->getProxyPHID()) { + unset($dst_columns[$key]); + } + } + $has_column = false; $dst_column = null; From 0561043a1f575567b9619637837e86723d977207 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 7 Aug 2019 09:04:36 -0700 Subject: [PATCH 081/122] In "Move task to..." workflow, separate visible and hidden columns in the dropdown Summary: Ref T13368. Currently, both visible and hidden columns are shown in the "Move tasks to..." dropdown on workflows from workboards. When the dropdown contains hidden columns, move them to a separate section to make it clear that they're not likely targets. Test Plan: - Used "Move tasks to project..." targeting a board with no hidden columns. Saw a single ungrouped dropdown. - Used "Move tasks to project..." targeting a board with hidden columns. Saw a dropdown grouped into "Visible" and "Hidden" columns. Maniphest Tasks: T13368 Differential Revision: https://secure.phabricator.com/D20700 --- ...ricatorProjectColumnBulkMoveController.php | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php index 5792664110..0931e4f7be 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnBulkMoveController.php @@ -219,12 +219,40 @@ final class PhabricatorProjectColumnBulkMoveController ->setValue($dst_project->getDisplayName())); } + $column_options = array( + 'visible' => array(), + 'hidden' => array(), + ); + + $any_hidden = false; + foreach ($dst_columns as $column) { + if (!$column->isHidden()) { + $group = 'visible'; + } else { + $group = 'hidden'; + } + + $phid = $column->getPHID(); + $display_name = $column->getDisplayName(); + + $column_options[$group][$phid] = $display_name; + } + + if ($column_options['hidden']) { + $column_options = array( + pht('Visible Columns') => $column_options['visible'], + pht('Hidden Columns') => $column_options['hidden'], + ); + } else { + $column_options = $column_options['visible']; + } + $form->appendControl( - id(new AphrontFormSelectControl()) - ->setName('dstColumnPHID') - ->setLabel(pht('Move to Column')) - ->setValue($dst_column_phid) - ->setOptions(mpull($dst_columns, 'getDisplayName', 'getPHID'))); + id(new AphrontFormSelectControl()) + ->setName('dstColumnPHID') + ->setLabel(pht('Move to Column')) + ->setValue($dst_column_phid) + ->setOptions($column_options)); $submit = pht('Move Tasks'); From 937edcdc580cb720c9690c7071edc73acc9cdc4d Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 7 Aug 2019 09:04:39 -0700 Subject: [PATCH 082/122] Fix a warning in BoardLayoutEngine when no objects are being updated Summary: Fixes T13368. Some workflows (like "Move tasks to...") execute board layout without objects to update. In these cases, we can hit a warning because `objectPHIDs` is not initialized to `array()`. Test Plan: Went through the "Move tasks to..." workflow on a workboard, no longer saw a warning when trying to iterate over an empty `objectPHIDs` list. Maniphest Tasks: T13368 Differential Revision: https://secure.phabricator.com/D20701 --- .../project/engine/PhabricatorBoardLayoutEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 83fb943b76..22aaed2d4b 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -4,7 +4,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private $viewer; private $boardPHIDs; - private $objectPHIDs; + private $objectPHIDs = array(); private $boards; private $columnMap = array(); private $objectColumnMap = array(); From 46d9065bf148496777b73c8f97dee989c1774ab0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 8 Aug 2019 10:13:41 -0700 Subject: [PATCH 083/122] Drop test for awardable badges on "Badges" tab of user profiles to avoid overheating Summary: Fixes T13370. We currently show an "Award Badge" button conditionally, based on whether the viewer can award any badges or not. The query to test this may overheat and this pattern isn't consistent with other UI anyway. Stop doing this test. Test Plan: - Created 12 badges. - As a user who could not edit any of the badges, viewed the "Badges" section of a user profile. Maniphest Tasks: T13370 Differential Revision: https://secure.phabricator.com/D20702 --- ...abricatorPeopleProfileBadgesController.php | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php index f98970ef73..e4861e488a 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php @@ -34,20 +34,6 @@ final class PhabricatorPeopleProfileBadgesController $user, PhabricatorPeopleProfileMenuEngine::ITEM_BADGES); - // Best option? - $badges = id(new PhabricatorBadgesQuery()) - ->setViewer($viewer) - ->withStatuses(array( - PhabricatorBadgesBadge::STATUS_ACTIVE, - )) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->setLimit(1) - ->execute(); - $button = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') @@ -55,17 +41,16 @@ final class PhabricatorPeopleProfileBadgesController ->setWorkflow(true) ->setHref('/badges/award/'.$user->getID().'/'); - if ($badges) { - $header->addActionLink($button); - } + $header->addActionLink($button); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->addClass('project-view-home') ->addClass('project-view-people-home') - ->setFooter(array( - $this->buildBadgesView($user) - )); + ->setFooter( + array( + $badges, + )); return $this->newPage() ->setTitle($title) From 9bd74dfa6c076958ae0d23675812ff93b4685cc9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 8 Aug 2019 10:26:46 -0700 Subject: [PATCH 084/122] Autofocus the "App Code" input on the TOTP prompt during MFA gates after login Summary: See downstream . The "autofocus" attribute mostly just works, so add it to this input. Test Plan: As a user with TOTP enabled, established a new session. Saw browser automatically focus the "App Code" input on the TOTP prompt screen. Differential Revision: https://secure.phabricator.com/D20703 --- src/applications/auth/factor/PhabricatorTOTPAuthFactor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 7e77dfc11a..ebdf1b7218 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -194,6 +194,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $control = id(new PHUIFormNumberControl()) ->setName($name) ->setDisableAutocomplete(true) + ->setAutofocus(true) ->setValue($value) ->setError($error); } From 0a3c26998fc985a0cc660fe6a8e92184c6b08f69 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 8 Aug 2019 10:35:04 -0700 Subject: [PATCH 085/122] When the feed query on project profile pages overheats, contain the damage Summary: Ref T13349. This is almost the same change as D20678, but for project profiles instead of user profiles. The general reproduction case is "view a project where you can't see more than 50 of the 500 most recent feed stories". Test Plan: - Forced all queries to overheat. - Viewed a project profile page. - Before: overheating fatal near top level. - After: damage contained to feed panel. Maniphest Tasks: T13349 Differential Revision: https://secure.phabricator.com/D20704 --- .../PhabricatorProjectProfileController.php | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 67b94f7fa9..386a649238 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -78,14 +78,29 @@ final class PhabricatorProjectProfileController $project, PhabricatorProject::ITEM_PROFILE); - $stories = id(new PhabricatorFeedQuery()) + $query = id(new PhabricatorFeedQuery()) ->setViewer($viewer) - ->withFilterPHIDs( - array( - $project->getPHID(), - )) + ->withFilterPHIDs(array($project->getPHID())) ->setLimit(50) - ->execute(); + ->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, + )); + } $view_all = id(new PHUIButtonView()) ->setTag('a') @@ -103,7 +118,11 @@ final class PhabricatorProjectProfileController $feed = id(new PHUIObjectBoxView()) ->setHeader($feed_header) ->addClass('project-view-feed') - ->appendChild($feed); + ->appendChild( + array( + $overheated_view, + $feed, + )); require_celerity_resource('project-view-css'); From c092492a53e0e50df8019f765065ea4a9062f94a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 12 Aug 2019 11:11:26 -0700 Subject: [PATCH 086/122] Fix missing display cell in daemon summary table Summary: Fixes T13374. The "Temporary Failures" row is missing a cell definiton from the addition of "Average Queue Time". Test Plan: Viewed "/daemon/" with some temporary failures and and odd number of rows above the "Temporary Failures" row. Saw cell properly zebra-striped. Maniphest Tasks: T13374 Differential Revision: https://secure.phabricator.com/D20710 --- .../daemon/controller/PhabricatorDaemonConsoleController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index 421008082f..8d9a9986e4 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -85,6 +85,7 @@ final class PhabricatorDaemonConsoleController phutil_tag('em', array(), pht('Temporary Failures')), count($failed), null, + null, ); } From 006cb659cbf1a0153548c6fc287535b678addb48 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 12 Aug 2019 11:19:08 -0700 Subject: [PATCH 087/122] Make the success message from "bin/config" more clear Summary: Ref T13373. When you "bin/config set x ..." a value, the success message ("Set x ...") is somewhat ambiguous and can be interpreted as "First, you need to set x..." rather than "Success, wrote x...". Make the messaging more explicit. Also make this string more translatable. Test Plan: Ran `bin/config set ...` with various combinations of flags, saw more clear messaging. Maniphest Tasks: T13373 Differential Revision: https://secure.phabricator.com/D20711 --- ...PhabricatorConfigManagementSetWorkflow.php | 44 ++++++++++++------- .../env/PhabricatorConfigLocalSource.php | 5 +++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php index 22b760872e..9eb83bd61e 100644 --- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php @@ -30,11 +30,10 @@ final class PhabricatorConfigManagementSetWorkflow } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); $argv = $args->getArg('args'); - if (count($argv) == 0) { + if (!$argv) { throw new PhutilArgumentUsageException( - pht('Specify a configuration key and a value to set it to.')); + pht('Specify the configuration key you want to set.')); } $is_stdin = $args->getArg('stdin'); @@ -45,7 +44,8 @@ final class PhabricatorConfigManagementSetWorkflow if (count($argv) > 1) { throw new PhutilArgumentUsageException( pht( - 'Too many arguments: expected only a key when using "--stdin".')); + 'Too many arguments: expected only a configuration key when '. + 'using "--stdin".')); } fprintf(STDERR, tsprintf("%s\n", pht('Reading value from stdin...'))); @@ -54,7 +54,8 @@ final class PhabricatorConfigManagementSetWorkflow if (count($argv) == 1) { throw new PhutilArgumentUsageException( pht( - "Specify a value to set the key '%s' to.", + 'Specify a value to set the configuration key "%s" to, or '. + 'use "--stdin" to read a value from stdin.', $key)); } @@ -67,14 +68,13 @@ final class PhabricatorConfigManagementSetWorkflow $value = $argv[1]; } - $options = PhabricatorApplicationConfigOptions::loadAllOptions(); if (empty($options[$key])) { throw new PhutilArgumentUsageException( pht( - "No such configuration key '%s'! Use `%s` to list all keys.", - $key, - 'config list')); + 'Configuration key "%s" is unknown. Use "bin/config list" to list '. + 'all known keys.', + $key)); } $option = $options[$key]; @@ -99,7 +99,7 @@ final class PhabricatorConfigManagementSetWorkflow switch ($type) { default: $message = pht( - 'Config key "%s" is of type "%s". Specify it in JSON.', + 'Configuration key "%s" is of type "%s". Specify it in JSON.', $key, $type); break; @@ -128,7 +128,6 @@ final class PhabricatorConfigManagementSetWorkflow } if ($use_database) { - $config_type = 'database'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $config_entry->setValue($value); @@ -136,15 +135,28 @@ final class PhabricatorConfigManagementSetWorkflow $config_entry->setIsDeleted(0); $config_entry->save(); + + $write_message = pht( + 'Wrote configuration key "%s" to database storage.', + $key); } else { - $config_type = 'local'; - id(new PhabricatorConfigLocalSource()) + $config_source = id(new PhabricatorConfigLocalSource()) ->setKeys(array($key => $value)); + + $local_path = $config_source->getReadablePath(); + + $write_message = pht( + 'Wrote configuration key "%s" to local storage (in file "%s").', + $key, + $local_path); } - $console->writeOut( - "%s\n", - pht("Set '%s' in %s configuration.", $key, $config_type)); + echo tsprintf( + "** %s ** %s\n", + pht('DONE'), + $write_message); + + return 0; } } diff --git a/src/infrastructure/env/PhabricatorConfigLocalSource.php b/src/infrastructure/env/PhabricatorConfigLocalSource.php index 16dc43a9bc..fc1c83f812 100644 --- a/src/infrastructure/env/PhabricatorConfigLocalSource.php +++ b/src/infrastructure/env/PhabricatorConfigLocalSource.php @@ -65,4 +65,9 @@ final class PhabricatorConfigLocalSource extends PhabricatorConfigProxySource { return $path; } + public function getReadablePath() { + $path = $this->getConfigPath(); + return Filesystem::readablePath($path); + } + } From 82cf97ad65a910cb55e9c53c3e80f0740a724a10 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 12 Aug 2019 12:07:13 -0700 Subject: [PATCH 088/122] When many commits are discovered at once, import them at lower priority Summary: Ref T13369. See that task for discussion. When the discovery daemon finds more than 64 commits to import, demote the worker queue priority of the resulting tasks. Test Plan: - Pushed one commit, ran `bin/repository discover --verbose --trace ...`, saw commit import with "at normal priority" message and priority 2500 ("PRIORITY_COMMIT"). - Pushed 3 commits, set threshold to 3, ran `bin/repository discover ...`, saw commist import with "at lower priority" message and priority 4000 ("PRIORITY_IMPORT"). Maniphest Tasks: T13369 Differential Revision: https://secure.phabricator.com/D20712 --- .../PhabricatorRepositoryDiscoveryEngine.php | 106 +++++++++++++----- .../storage/PhabricatorRepository.php | 2 + 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index c1bb9190b3..790b95d775 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -93,6 +93,8 @@ final class PhabricatorRepositoryDiscoveryEngine // Clear the working set cache. $this->workingSet = array(); + $task_priority = $this->getImportTaskPriority($repository, $refs); + // Record discovered commits and mark them in the cache. foreach ($refs as $ref) { $this->recordCommit( @@ -100,7 +102,8 @@ final class PhabricatorRepositoryDiscoveryEngine $ref->getIdentifier(), $ref->getEpoch(), $ref->getCanCloseImmediately(), - $ref->getParents()); + $ref->getParents(), + $task_priority); $this->commitCache[$ref->getIdentifier()] = true; } @@ -536,7 +539,8 @@ final class PhabricatorRepositoryDiscoveryEngine $commit_identifier, $epoch, $close_immediately, - array $parents) { + array $parents, + $task_priority) { $commit = new PhabricatorRepositoryCommit(); $conn_w = $repository->establishConnection('w'); @@ -559,7 +563,7 @@ final class PhabricatorRepositoryDiscoveryEngine $commit_identifier); // After reviving a commit, schedule new daemons for it. - $this->didDiscoverCommit($repository, $commit, $epoch); + $this->didDiscoverCommit($repository, $commit, $epoch, $task_priority); return; } @@ -620,7 +624,7 @@ final class PhabricatorRepositoryDiscoveryEngine } $commit->saveTransaction(); - $this->didDiscoverCommit($repository, $commit, $epoch); + $this->didDiscoverCommit($repository, $commit, $epoch, $task_priority); if ($this->repairMode) { // Normally, the query should throw a duplicate key exception. If we @@ -648,9 +652,10 @@ final class PhabricatorRepositoryDiscoveryEngine private function didDiscoverCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, - $epoch) { + $epoch, + $task_priority) { - $this->insertTask($repository, $commit); + $this->insertTask($repository, $commit, $task_priority); // Update the repository summary table. queryfx( @@ -677,6 +682,7 @@ final class PhabricatorRepositoryDiscoveryEngine private function insertTask( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, + $task_priority, $data = array()) { $vcs = $repository->getVersionControlSystem(); @@ -696,27 +702,6 @@ final class PhabricatorRepositoryDiscoveryEngine $data['commitID'] = $commit->getID(); - // If the repository is importing for the first time, we schedule tasks - // at IMPORT priority, which is very low. Making progress on importing a - // new repository for the first time is less important than any other - // daemon task. - - // If the repository has finished importing and we're just catching up - // on recent commits, we schedule discovery at COMMIT priority, which is - // slightly below the default priority. - - // Note that followup tasks and triggered tasks (like those generated by - // Herald or Harbormaster) will queue at DEFAULT priority, so that each - // commit tends to fully import before we start the next one. This tends - // to give imports fairly predictable progress. See T11677 for some - // discussion. - - if ($repository->isImporting()) { - $task_priority = PhabricatorWorker::PRIORITY_IMPORT; - } else { - $task_priority = PhabricatorWorker::PRIORITY_COMMIT; - } - $options = array( 'priority' => $task_priority, ); @@ -934,4 +919,71 @@ final class PhabricatorRepositoryDiscoveryEngine $data['epoch']); } + private function getImportTaskPriority( + PhabricatorRepository $repository, + array $refs) { + + // If the repository is importing for the first time, we schedule tasks + // at IMPORT priority, which is very low. Making progress on importing a + // new repository for the first time is less important than any other + // daemon task. + + // If the repository has finished importing and we're just catching up + // on recent commits, we usually schedule discovery at COMMIT priority, + // which is slightly below the default priority. + + // Note that followup tasks and triggered tasks (like those generated by + // Herald or Harbormaster) will queue at DEFAULT priority, so that each + // commit tends to fully import before we start the next one. This tends + // to give imports fairly predictable progress. See T11677 for some + // discussion. + + if ($repository->isImporting()) { + $this->log( + pht( + 'Importing %s commit(s) at low priority ("PRIORITY_IMPORT") '. + 'because this repository is still importing.', + phutil_count($refs))); + + return PhabricatorWorker::PRIORITY_IMPORT; + } + + // See T13369. If we've discovered a lot of commits at once, import them + // at lower priority. + + // This is mostly aimed at reducing the impact that synchronizing thousands + // of commits from a remote upstream has on other repositories. The queue + // is "mostly FIFO", so queueing a thousand commit imports can stall other + // repositories. + + // In a perfect world we'd probably give repositories round-robin queue + // priority, but we don't currently have the primitives for this and there + // isn't a strong case for building them. + + // Use "a whole lot of commits showed up at once" as a heuristic for + // detecting "someone synchronized an upstream", and import them at a lower + // priority to more closely approximate fair scheduling. + + if (count($refs) >= PhabricatorRepository::LOWPRI_THRESHOLD) { + $this->log( + pht( + 'Importing %s commit(s) at low priority ("PRIORITY_IMPORT") '. + 'because many commits were discovered at once.', + phutil_count($refs))); + + return PhabricatorWorker::PRIORITY_IMPORT; + } + + // Otherwise, import at normal priority. + + if ($refs) { + $this->log( + pht( + 'Importing %s commit(s) at normal priority ("PRIORITY_COMMIT").', + phutil_count($refs))); + } + + return PhabricatorWorker::PRIORITY_COMMIT; + } + } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 30eb56cfd5..9661584851 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -34,6 +34,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO */ const IMPORT_THRESHOLD = 7; + const LOWPRI_THRESHOLD = 64; + const TABLE_PATH = 'repository_path'; const TABLE_PATHCHANGE = 'repository_pathchange'; const TABLE_FILESYSTEM = 'repository_filesystem'; From d890c03ac348b3f9ced3ca73077e5934f18b7a6e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 15 Aug 2019 12:00:16 -0700 Subject: [PATCH 089/122] Namespace all column references in ProjectQuery to fix ambiguity with Ferret constraints Summary: Fixes T13378. If we join Ferret tables and page, we can end up with an ambiguous `id` column here. Explicitly refer to "project.x" in all cases that we're interacting with the project table. Test Plan: - Changed page size to 3. - Issued a Projects query for "~e", matching more than 3 results. - Clicked "Next Page". - Before: ambiguous id column fatal. - After: next page. Maniphest Tasks: T13378 Differential Revision: https://secure.phabricator.com/D20714 --- .../project/query/PhabricatorProjectQuery.php | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index b08a58501f..b68c638363 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -464,28 +464,28 @@ final class PhabricatorProjectQuery } $where[] = qsprintf( $conn, - 'status IN (%Ld)', + 'project.status IN (%Ld)', $filter); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, - 'status IN (%Ls)', + 'project.status IN (%Ls)', $this->statuses); } if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'project.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'project.phid IN (%Ls)', $this->phids); } @@ -513,7 +513,7 @@ final class PhabricatorProjectQuery if ($this->names !== null) { $where[] = qsprintf( $conn, - 'name IN (%Ls)', + 'project.name IN (%Ls)', $this->names); } @@ -522,7 +522,7 @@ final class PhabricatorProjectQuery foreach ($this->namePrefixes as $name_prefix) { $parts[] = qsprintf( $conn, - 'name LIKE %>', + 'project.name LIKE %>', $name_prefix); } $where[] = qsprintf($conn, '%LO', $parts); @@ -531,21 +531,21 @@ final class PhabricatorProjectQuery if ($this->icons !== null) { $where[] = qsprintf( $conn, - 'icon IN (%Ls)', + 'project.icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn, - 'color IN (%Ls)', + 'project.color IN (%Ls)', $this->colors); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, - 'parentProjectPHID IN (%Ls)', + 'project.parentProjectPHID IN (%Ls)', $this->parentPHIDs); } @@ -563,7 +563,7 @@ final class PhabricatorProjectQuery foreach ($ancestor_paths as $ancestor_path) { $sql[] = qsprintf( $conn, - '(projectPath LIKE %> AND projectDepth > %d)', + '(project.projectPath LIKE %> AND project.projectDepth > %d)', $ancestor_path['projectPath'], $ancestor_path['projectDepth']); } @@ -572,18 +572,18 @@ final class PhabricatorProjectQuery $where[] = qsprintf( $conn, - 'parentProjectPHID IS NOT NULL'); + 'project.parentProjectPHID IS NOT NULL'); } if ($this->isMilestone !== null) { if ($this->isMilestone) { $where[] = qsprintf( $conn, - 'milestoneNumber IS NOT NULL'); + 'project.milestoneNumber IS NOT NULL'); } else { $where[] = qsprintf( $conn, - 'milestoneNumber IS NULL'); + 'project.milestoneNumber IS NULL'); } } @@ -591,42 +591,42 @@ final class PhabricatorProjectQuery if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, - 'hasSubprojects = %d', + 'project.hasSubprojects = %d', (int)$this->hasSubprojects); } if ($this->minDepth !== null) { $where[] = qsprintf( $conn, - 'projectDepth >= %d', + 'project.projectDepth >= %d', $this->minDepth); } if ($this->maxDepth !== null) { $where[] = qsprintf( $conn, - 'projectDepth <= %d', + 'project.projectDepth <= %d', $this->maxDepth); } if ($this->minMilestoneNumber !== null) { $where[] = qsprintf( $conn, - 'milestoneNumber >= %d', + 'project.milestoneNumber >= %d', $this->minMilestoneNumber); } if ($this->maxMilestoneNumber !== null) { $where[] = qsprintf( $conn, - 'milestoneNumber <= %d', + 'project.milestoneNumber <= %d', $this->maxMilestoneNumber); } if ($this->subtypes !== null) { $where[] = qsprintf( $conn, - 'subtype IN (%Ls)', + 'project.subtype IN (%Ls)', $this->subtypes); } @@ -646,7 +646,7 @@ final class PhabricatorProjectQuery if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, - 'JOIN %T e ON e.src = p.phid AND e.type = %d', + 'JOIN %T e ON e.src = project.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); } @@ -654,7 +654,7 @@ final class PhabricatorProjectQuery if ($this->watcherPHIDs !== null) { $joins[] = qsprintf( $conn, - 'JOIN %T w ON w.src = p.phid AND w.type = %d', + 'JOIN %T w ON w.src = project.phid AND w.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasWatcherEdgeType::EDGECONST); } @@ -662,7 +662,7 @@ final class PhabricatorProjectQuery if ($this->slugs !== null) { $joins[] = qsprintf( $conn, - 'JOIN %T slug on slug.projectPHID = p.phid', + 'JOIN %T slug on slug.projectPHID = project.phid', id(new PhabricatorProjectSlug())->getTableName()); } @@ -672,7 +672,7 @@ final class PhabricatorProjectQuery $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, - 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', + 'JOIN %T %T ON %T.projectID = project.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, @@ -689,7 +689,7 @@ final class PhabricatorProjectQuery } protected function getPrimaryTableAlias() { - return 'p'; + return 'project'; } private function linkProjectGraph(array $projects, array $ancestors) { From 803eb29c71d35f956b11d6380bc414d341de826c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 20 Aug 2019 12:28:52 -0700 Subject: [PATCH 090/122] Fix flag typo in "Managing Caches" documentation Summary: See PHI1392. This flag is `--all`, not `--all-caches`. Test Plan: Ran `bin/cache purge --all`. Differential Revision: https://secure.phabricator.com/D20722 --- src/docs/user/configuration/managing_caches.diviner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/user/configuration/managing_caches.diviner b/src/docs/user/configuration/managing_caches.diviner index 53b48250ea..e873b99d8c 100644 --- a/src/docs/user/configuration/managing_caches.diviner +++ b/src/docs/user/configuration/managing_caches.diviner @@ -41,7 +41,7 @@ with the `--help` flag to see options: This tool can purge caches in a granular way, but it's normally easiest to just purge all of the caches: - phabricator/ $ ./bin/cache purge --purge-all + phabricator/ $ ./bin/cache purge --all You can purge caches safely. The data they contain can always be rebuilt from other data if Phabricator needs it. From 721a86401ff454f524481dd4e5fc893cd6ad8d07 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 20 Aug 2019 12:46:09 -0700 Subject: [PATCH 091/122] Implement "drydock.resource.search" Summary: Fixes T13383. Provide a basic "drydock.resource.search". Also allow "drydock.lease.search" to be queried by resource PHID. Test Plan: Called "drydock.resource.search" and "drydock.lease.search" with various constraints. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13383 Differential Revision: https://secure.phabricator.com/D20723 --- src/__phutil_library_map__.php | 3 ++ .../DrydockResourceSearchConduitAPIMethod.php | 18 +++++++++ .../query/DrydockLeaseSearchEngine.php | 9 +++++ .../drydock/query/DrydockResourceQuery.php | 16 +++++--- .../query/DrydockResourceSearchEngine.php | 10 +++++ .../drydock/storage/DrydockResource.php | 38 ++++++++++++++++++- 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 03e0163ec8..a0072f7cea 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1222,6 +1222,7 @@ phutil_register_library_map(array( 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', 'DrydockResourceReclaimLogType' => 'applications/drydock/logtype/DrydockResourceReclaimLogType.php', 'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php', + 'DrydockResourceSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php', 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php', @@ -7021,6 +7022,7 @@ phutil_register_library_map(array( 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', + 'PhabricatorConduitResultInterface', ), 'DrydockResourceActivationFailureLogType' => 'DrydockLogType', 'DrydockResourceActivationYieldLogType' => 'DrydockLogType', @@ -7034,6 +7036,7 @@ phutil_register_library_map(array( 'DrydockResourceQuery' => 'DrydockQuery', 'DrydockResourceReclaimLogType' => 'DrydockLogType', 'DrydockResourceReleaseController' => 'DrydockResourceController', + 'DrydockResourceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'PhabricatorObjectStatus', 'DrydockResourceUpdateWorker' => 'DrydockWorker', diff --git a/src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php new file mode 100644 index 0000000000..03363b0542 --- /dev/null +++ b/src/applications/drydock/conduit/DrydockResourceSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +withOwnerPHIDs($map['ownerPHIDs']); } + if ($map['resourcePHIDs']) { + $query->withResourcePHIDs($map['resourcePHIDs']); + } + return $query; } @@ -58,6 +62,11 @@ final class DrydockLeaseSearchEngine ->setKey('ownerPHIDs') ->setAliases(array('owner', 'owners', 'ownerPHID')) ->setDescription(pht('Search leases by owner.')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Resources')) + ->setKey('resourcePHIDs') + ->setAliases(array('resorucePHID', 'resource', 'resources')) + ->setDescription(pht('Search leases by resource.')), ); } diff --git a/src/applications/drydock/query/DrydockResourceQuery.php b/src/applications/drydock/query/DrydockResourceQuery.php index c477da20b5..bcbff03663 100644 --- a/src/applications/drydock/query/DrydockResourceQuery.php +++ b/src/applications/drydock/query/DrydockResourceQuery.php @@ -100,46 +100,50 @@ final class DrydockResourceQuery extends DrydockQuery { if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'resource.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'resource.phid IN (%Ls)', $this->phids); } if ($this->types !== null) { $where[] = qsprintf( $conn, - 'type IN (%Ls)', + 'resource.type IN (%Ls)', $this->types); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, - 'status IN (%Ls)', + 'resource.status IN (%Ls)', $this->statuses); } if ($this->blueprintPHIDs !== null) { $where[] = qsprintf( $conn, - 'blueprintPHID IN (%Ls)', + 'resource.blueprintPHID IN (%Ls)', $this->blueprintPHIDs); } if ($this->datasourceQuery !== null) { $where[] = qsprintf( $conn, - 'name LIKE %>', + 'resource.name LIKE %>', $this->datasourceQuery); } return $where; } + protected function getPrimaryTableAlias() { + return 'resource'; + } + } diff --git a/src/applications/drydock/query/DrydockResourceSearchEngine.php b/src/applications/drydock/query/DrydockResourceSearchEngine.php index 8f72fdf217..9015f92408 100644 --- a/src/applications/drydock/query/DrydockResourceSearchEngine.php +++ b/src/applications/drydock/query/DrydockResourceSearchEngine.php @@ -40,6 +40,10 @@ final class DrydockResourceSearchEngine $query->withStatuses($map['statuses']); } + if ($map['blueprintPHIDs']) { + $query->withBlueprintPHIDs($map['blueprintPHIDs']); + } + return $query; } @@ -49,6 +53,12 @@ final class DrydockResourceSearchEngine ->setLabel(pht('Statuses')) ->setKey('statuses') ->setOptions(DrydockResourceStatus::getStatusMap()), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Blueprints')) + ->setKey('blueprintPHIDs') + ->setAliases(array('blueprintPHID', 'blueprints', 'blueprint')) + ->setDescription( + pht('Search for resources generated by particular blueprints.')), ); } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 8ec63cb097..bc672dba3c 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,7 +1,9 @@ setKey('blueprintPHID') + ->setType('phid') + ->setDescription(pht('The blueprint which generated this resource.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('status') + ->setType('map') + ->setDescription(pht('Information about resource status.')), + ); + } + + public function getFieldValuesForConduit() { + $status = $this->getStatus(); + + return array( + 'blueprintPHID' => $this->getBlueprintPHID(), + 'status' => array( + 'value' => $status, + 'name' => DrydockResourceStatus::getNameForStatus($status), + ), + ); + } + + public function getConduitSearchAttachments() { + return array(); + } + } From fc34554892ec142d28b9e261dc529057773301e6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 20 Aug 2019 13:50:03 -0700 Subject: [PATCH 092/122] Replace "bin/people profileimage" with "bin/user enable|empower" Summary: Ref T13382. - Remove "bin/people profileimage" which previously generated profile image caches but now feels obsolete. - Replace it with "bin/user", with "enable" and "empower" flows. This command is now focused on regaining access to an install after you lock your keys inside. - Document the various ways to unlock objects and accounts from the CLI. Test Plan: - Ran `bin/user enable` and `bin/user empower` with various flags. - Grepped for `people profileimage` and found no references. - Grepped for `bin/people` and found no references. - Read documentation. Maniphest Tasks: T13382 Differential Revision: https://secure.phabricator.com/D20724 --- bin/people | 1 - bin/user | 1 + .../manage_user.php} | 4 +- src/__phutil_library_map__.php | 6 +- ...ricatorPeopleManagementEmpowerWorkflow.php | 44 +++++++ ...bricatorPeopleManagementEnableWorkflow.php | 44 +++++++ .../PhabricatorPeopleManagementWorkflow.php | 76 +++++++----- .../PhabricatorPeopleProfileImageWorkflow.php | 85 ------------- src/docs/user/userguide/unlocking.diviner | 116 ++++++++++++++++++ 9 files changed, 254 insertions(+), 123 deletions(-) delete mode 120000 bin/people create mode 120000 bin/user rename scripts/{people/manage_people.php => setup/manage_user.php} (83%) create mode 100644 src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php create mode 100644 src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php delete mode 100644 src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php create mode 100644 src/docs/user/userguide/unlocking.diviner diff --git a/bin/people b/bin/people deleted file mode 120000 index 985dc4849a..0000000000 --- a/bin/people +++ /dev/null @@ -1 +0,0 @@ -../scripts/people/manage_people.php \ No newline at end of file diff --git a/bin/user b/bin/user new file mode 120000 index 0000000000..4b4b6b7ab5 --- /dev/null +++ b/bin/user @@ -0,0 +1 @@ +../scripts/setup/manage_user.php \ No newline at end of file diff --git a/scripts/people/manage_people.php b/scripts/setup/manage_user.php similarity index 83% rename from scripts/people/manage_people.php rename to scripts/setup/manage_user.php index aeada86da0..f571cb9346 100755 --- a/scripts/people/manage_people.php +++ b/scripts/setup/manage_user.php @@ -6,8 +6,8 @@ require_once $root.'/scripts/__init_script__.php'; $args = new PhutilArgumentParser($argv); $args->setSynopsis(<< 'applications/people/mail/PhabricatorPeopleMailEngine.php', 'PhabricatorPeopleMailEngineException' => 'applications/people/mail/PhabricatorPeopleMailEngineException.php', 'PhabricatorPeopleManageProfileMenuItem' => 'applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php', + 'PhabricatorPeopleManagementEmpowerWorkflow' => 'applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php', + 'PhabricatorPeopleManagementEnableWorkflow' => 'applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php', 'PhabricatorPeopleManagementWorkflow' => 'applications/people/management/PhabricatorPeopleManagementWorkflow.php', 'PhabricatorPeopleNewController' => 'applications/people/controller/PhabricatorPeopleNewController.php', 'PhabricatorPeopleNoOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleNoOwnerDatasource.php', @@ -4060,7 +4062,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleProfileCommitsController' => 'applications/people/controller/PhabricatorPeopleProfileCommitsController.php', 'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php', 'PhabricatorPeopleProfileEditController' => 'applications/people/controller/PhabricatorPeopleProfileEditController.php', - 'PhabricatorPeopleProfileImageWorkflow' => 'applications/people/management/PhabricatorPeopleProfileImageWorkflow.php', 'PhabricatorPeopleProfileManageController' => 'applications/people/controller/PhabricatorPeopleProfileManageController.php', 'PhabricatorPeopleProfileMenuEngine' => 'applications/people/engine/PhabricatorPeopleProfileMenuEngine.php', 'PhabricatorPeopleProfilePictureController' => 'applications/people/controller/PhabricatorPeopleProfilePictureController.php', @@ -10332,6 +10333,8 @@ phutil_register_library_map(array( 'PhabricatorPeopleMailEngine' => 'Phobject', 'PhabricatorPeopleMailEngineException' => 'Exception', 'PhabricatorPeopleManageProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorPeopleManagementEmpowerWorkflow' => 'PhabricatorPeopleManagementWorkflow', + 'PhabricatorPeopleManagementEnableWorkflow' => 'PhabricatorPeopleManagementWorkflow', 'PhabricatorPeopleManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPeopleNewController' => 'PhabricatorPeopleController', 'PhabricatorPeopleNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource', @@ -10341,7 +10344,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleProfileCommitsController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileEditController' => 'PhabricatorPeopleProfileController', - 'PhabricatorPeopleProfileImageWorkflow' => 'PhabricatorPeopleManagementWorkflow', 'PhabricatorPeopleProfileManageController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorPeopleProfilePictureController' => 'PhabricatorPeopleProfileController', diff --git a/src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php b/src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php new file mode 100644 index 0000000000..0393a96224 --- /dev/null +++ b/src/applications/people/management/PhabricatorPeopleManagementEmpowerWorkflow.php @@ -0,0 +1,44 @@ +getUserSelectionArguments(), + array()); + + $this + ->setName('empower') + ->setExamples('**empower** --user __username__') + ->setSynopsis(pht('Turn a user account into an administrator account.')) + ->setArguments($arguments); + } + + public function execute(PhutilArgumentParser $args) { + $user = $this->selectUser($args); + $display_name = $user->getUsername(); + + if ($user->getIsAdmin()) { + throw new PhutilArgumentUsageException( + pht( + 'User account "%s" is already an administrator. You can only '. + 'empower accounts that are not yet administrators.', + $display_name)); + } + + $xactions = array(); + $xactions[] = $user->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $this->applyTransactions($user, $xactions); + + $this->logOkay( + pht('DONE'), + pht('Empowered user account "%s".', $display_name)); + + return 0; + } + +} diff --git a/src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php b/src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php new file mode 100644 index 0000000000..b721bf221e --- /dev/null +++ b/src/applications/people/management/PhabricatorPeopleManagementEnableWorkflow.php @@ -0,0 +1,44 @@ +getUserSelectionArguments(), + array()); + + $this + ->setName('enable') + ->setExamples('**enable** --user __username__') + ->setSynopsis(pht('Enable a disabled user account.')) + ->setArguments($arguments); + } + + public function execute(PhutilArgumentParser $args) { + $user = $this->selectUser($args); + $display_name = $user->getUsername(); + + if (!$user->getIsDisabled()) { + throw new PhutilArgumentUsageException( + pht( + 'User account "%s" is not disabled. You can only enable accounts '. + 'that are disabled.', + $display_name)); + } + + $xactions = array(); + $xactions[] = $user->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorUserDisableTransaction::TRANSACTIONTYPE) + ->setNewValue(false); + + $this->applyTransactions($user, $xactions); + + $this->logOkay( + pht('DONE'), + pht('Enabled user account "%s".', $display_name)); + + return 0; + } + +} diff --git a/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php b/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php index 67b474f7e2..d504b8c11b 100644 --- a/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php +++ b/src/applications/people/management/PhabricatorPeopleManagementWorkflow.php @@ -3,45 +3,55 @@ abstract class PhabricatorPeopleManagementWorkflow extends PhabricatorManagementWorkflow { - protected function buildIterator(PhutilArgumentParser $args) { - $usernames = $args->getArg('users'); - - if ($args->getArg('all')) { - if ($usernames) { - throw new PhutilArgumentUsageException( - pht( - 'Specify either a list of users or `%s`, but not both.', - '--all')); - } - return new LiskMigrationIterator(new PhabricatorUser()); - } - - if ($usernames) { - return $this->loadUsersWithUsernames($usernames); - } - - return null; + final protected function getUserSelectionArguments() { + return array( + array( + 'name' => 'user', + 'param' => 'username', + 'help' => pht('User account to act on.'), + ), + ); } - protected function loadUsersWithUsernames(array $usernames) { - $users = array(); - foreach($usernames as $username) { - $query = id(new PhabricatorPeopleQuery()) - ->setViewer($this->getViewer()) - ->withUsernames(array($username)) - ->executeOne(); + final protected function selectUser(PhutilArgumentParser $argv) { + $username = $argv->getArg('user'); - if (!$query) { - throw new PhutilArgumentUsageException( - pht( - '"%s" is not a valid username.', - $username)); - } - $users[] = $query; + if (!strlen($username)) { + throw new PhutilArgumentUsageException( + pht( + 'Select a user account to act on with "--user ".')); } - return $users; + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withUsernames(array($username)) + ->executeOne(); + if (!$user) { + throw new PhutilArgumentUsageException( + pht( + 'No user with username "%s" exists.', + $username)); + } + + return $user; } + final protected function applyTransactions( + PhabricatorUser $user, + array $xactions) { + assert_instances_of($xactions, 'PhabricatorUserTransaction'); + + $viewer = $this->getViewer(); + $application = id(new PhabricatorPeopleApplication())->getPHID(); + $content_source = $this->newContentSource(); + + $editor = $user->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($application) + ->setContentSource($content_source) + ->setContinueOnMissingFields(true); + + return $editor->applyTransactions($user, $xactions); + } } diff --git a/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php b/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php deleted file mode 100644 index 8bf3c8e118..0000000000 --- a/src/applications/people/management/PhabricatorPeopleProfileImageWorkflow.php +++ /dev/null @@ -1,85 +0,0 @@ -setName('profileimage') - ->setExamples('**profileimage** --users __username__') - ->setSynopsis(pht('Generate default profile images.')) - ->setArguments( - array( - array( - 'name' => 'all', - 'help' => pht( - 'Generate default profile images for all users.'), - ), - array( - 'name' => 'force', - 'short' => 'f', - 'help' => pht( - 'Force a default profile image to be replaced.'), - ), - array( - 'name' => 'users', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $is_force = $args->getArg('force'); - $is_all = $args->getArg('all'); - - $gd = function_exists('imagecreatefromstring'); - if (!$gd) { - throw new PhutilArgumentUsageException( - pht( - 'GD is not installed for php-cli. Aborting.')); - } - - $iterator = $this->buildIterator($args); - if (!$iterator) { - throw new PhutilArgumentUsageException( - pht( - 'Either specify a list of users to update, or use `%s` '. - 'to update all users.', - '--all')); - } - - $version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION; - $generator = new PhabricatorFilesComposeAvatarBuiltinFile(); - - foreach ($iterator as $user) { - $username = $user->getUsername(); - $default_phid = $user->getDefaultProfileImagePHID(); - $gen_version = $user->getDefaultProfileImageVersion(); - - $generate = false; - if ($gen_version != $version) { - $generate = true; - } - - if ($default_phid == null || $is_force || $generate) { - $console->writeOut( - "%s\n", - pht( - 'Generating profile image for "%s".', - $username)); - - $generator->updateUser($user); - } else { - $console->writeOut( - "%s\n", - pht( - 'Default profile image "%s" already set for "%s".', - $version, - $username)); - } - } - } - -} diff --git a/src/docs/user/userguide/unlocking.diviner b/src/docs/user/userguide/unlocking.diviner new file mode 100644 index 0000000000..7dc29f69bd --- /dev/null +++ b/src/docs/user/userguide/unlocking.diviner @@ -0,0 +1,116 @@ +@title User Guide: Unlocking Objects +@group userguide + +Explains how to access locked or invisible objects and accounts. + +Overview +======== + +Phabricator tries to make it difficult for users to lock themselves out of +things, but you can occasionally end up in situations where no one has access +to an object that you need access to. + +For example, sometimes the only user who had edit permission for something has +left the organization, or you configured a "Phase of the Moon" policy rule and +the stars aren't currently aligned. + +You can use various CLI tools to unlock objects and accounts if you need to +regain access. + + +Unlocking Accounts +================== + +If you need to regain access to an object, the easiest approach is usually to +recover access to the account which owns it, then change the object policies +to be more open using the web UI. + +For example, if an important task was accidentally locked so that only a user +who is currently on vacation can edit it, you can log in as that user and +change the edit policy to something more permissive. + +To regain access to an account: + +``` +$ ./bin/auth recover +``` + +If the account you're recovering access to has MFA or other session prompts, +use the `--force-full-session` to bypass them: + +``` +$ ./bin/auth recover --force-full-session +``` + +In either case, the command will give you a link you a one-time link you can +use to access the account from the web UI. From there, you can open up objects +or change settings. + + +Unlocking MFA +============= + +You can completely strip MFA from a user account with: + +``` +$ ./bin/auth strip --user ... +``` + +For detailed help on managing and stripping MFA, see the instructions in +@{article:User Guide: Multi-Factor Authentication} + + +Unlocking Objects +================= + +If you aren't sure who owns an object, or no user account has access to an +object, you can directly change object policies from the CLI: + +``` +$ ./bin/policy unlock [--view ...] [--edit ...] [--owner ...] +``` + +To identify the object you want to unlock, you can specify an object name (like +`T123`) or a PHID as the `` parameter. + +Use the `--view` and `--edit` flags (and, for some objects, the `--owner` +flag) to specify new policies for the object. + +For example, to make task `T123` editable by user `@alice`, run: + +``` +$ ./bin/policy unlock T123 --edit alice +``` + +Not every object has mutable view and edit policies, and not every object has +an owner, so each flag only works on some types of objects. + +From here, you can log in to the web UI and change the relevant policies to +whatever you want to set them to. + + +No Enabled Users +================ + +If you accidentally disabled all administrator accounts, you can enable a +disabled account from the CLI like this: + +``` +$ ./bin/user enable --user +``` + +From here, recover the account or log in normally. + + +No Administrators +================= + +If you accidentally deleted all the administrator accounts, you can empower +a user as an administrator from the CLI like this: + +``` +$ ./bin/user empower --user +``` + +This will upgrade the user account from a regular account to an administrator +account. From 64b399d9be2684c4dad39fe8bab482434cd9fcf1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 20 Aug 2019 17:41:33 -0700 Subject: [PATCH 093/122] Remove "bin/accountadmin" and "scripts/user/add_user.php" Summary: Fixes T13382. Depends on D20724. These ancient scripts are no longer necessary since we've had a smooth web-based onboarding process for a long time. I retained `bin/user empower` and `bin/user enable` for recovering from situations where you accidentally delete or disable all administrators. This is normally difficult, but some users are industrious. Test Plan: Grepped for `accountadmin` and `add_user.php`, found no more hits. Maniphest Tasks: T13382 Differential Revision: https://secure.phabricator.com/D20725 --- bin/accountadmin | 1 - scripts/user/account_admin.php | 228 ------------------ scripts/user/add_user.php | 73 ------ ...figuring_accounts_and_registration.diviner | 26 +- 4 files changed, 2 insertions(+), 326 deletions(-) delete mode 120000 bin/accountadmin delete mode 100755 scripts/user/account_admin.php delete mode 100755 scripts/user/add_user.php diff --git a/bin/accountadmin b/bin/accountadmin deleted file mode 120000 index a846766c26..0000000000 --- a/bin/accountadmin +++ /dev/null @@ -1 +0,0 @@ -../scripts/user/account_admin.php \ No newline at end of file diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php deleted file mode 100755 index 4e4500a2f7..0000000000 --- a/scripts/user/account_admin.php +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env php -establishConnection('r'), - 'SELECT * FROM %T LIMIT 1', - $table->getTableName()); -$is_first_user = (!$any_user); - -if ($is_first_user) { - echo pht( - "WARNING\n\n". - "You're about to create the first account on this install. Normally, ". - "you should use the web interface to create the first account, not ". - "this script.\n\n". - "If you use the web interface, it will drop you into a nice UI workflow ". - "which gives you more help setting up your install. If you create an ". - "account with this script instead, you will skip the setup help and you ". - "will not be able to access it later."); - if (!phutil_console_confirm(pht('Skip easy setup and create account?'))) { - echo pht('Cancelled.')."\n"; - exit(1); - } -} - -echo pht( - 'Enter a username to create a new account or edit an existing account.'); - -$username = phutil_console_prompt(pht('Enter a username:')); -if (!strlen($username)) { - echo pht('Cancelled.')."\n"; - exit(1); -} - -if (!PhabricatorUser::validateUsername($username)) { - $valid = PhabricatorUser::describeValidUsername(); - echo pht("The username '%s' is invalid. %s", $username, $valid)."\n"; - exit(1); -} - - -$user = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $username); - -if (!$user) { - $original = new PhabricatorUser(); - - echo pht("There is no existing user account '%s'.", $username)."\n"; - $ok = phutil_console_confirm( - pht("Do you want to create a new '%s' account?", $username), - $default_no = false); - if (!$ok) { - echo pht('Cancelled.')."\n"; - exit(1); - } - $user = new PhabricatorUser(); - $user->setUsername($username); - - $is_new = true; -} else { - $original = clone $user; - - echo pht("There is an existing user account '%s'.", $username)."\n"; - $ok = phutil_console_confirm( - pht("Do you want to edit the existing '%s' account?", $username), - $default_no = false); - if (!$ok) { - echo pht('Cancelled.')."\n"; - exit(1); - } - - $is_new = false; -} - -$user_realname = $user->getRealName(); -if (strlen($user_realname)) { - $realname_prompt = ' ['.$user_realname.']:'; -} else { - $realname_prompt = ':'; -} -$realname = nonempty( - phutil_console_prompt(pht('Enter user real name').$realname_prompt), - $user_realname); -$user->setRealName($realname); - -// When creating a new user we prompt for an email address; when editing an -// existing user we just skip this because it would be quite involved to provide -// a reasonable CLI interface for editing multiple addresses and managing email -// verification and primary addresses. - -$create_email = null; -if ($is_new) { - do { - $email = phutil_console_prompt(pht('Enter user email address:')); - $duplicate = id(new PhabricatorUserEmail())->loadOneWhere( - 'address = %s', - $email); - if ($duplicate) { - echo pht( - "ERROR: There is already a user with that email address. ". - "Each user must have a unique email address.\n"); - } else { - break; - } - } while (true); - - $create_email = $email; -} - -$is_system_agent = $user->getIsSystemAgent(); -$set_system_agent = phutil_console_confirm( - pht('Is this user a bot?'), - $default_no = !$is_system_agent); - -$verify_email = null; -$set_verified = false; -// Allow administrators to verify primary email addresses at this time in edit -// scenarios. (Create will work just fine from here as we auto-verify email -// on create.) -if (!$is_new) { - $verify_email = $user->loadPrimaryEmail(); - if (!$verify_email->getIsVerified()) { - $set_verified = phutil_console_confirm( - pht('Should the primary email address be verified?'), - $default_no = true); - } else { - // Already verified so let's not make a fuss. - $verify_email = null; - } -} - -$is_admin = $user->getIsAdmin(); -$set_admin = phutil_console_confirm( - pht('Should this user be an administrator?'), - $default_no = !$is_admin); - -echo "\n\n".pht('ACCOUNT SUMMARY')."\n\n"; -$tpl = "%12s %-30s %-30s\n"; -printf($tpl, null, pht('OLD VALUE'), pht('NEW VALUE')); -printf($tpl, pht('Username'), $original->getUsername(), $user->getUsername()); -printf($tpl, pht('Real Name'), $original->getRealName(), $user->getRealName()); -if ($is_new) { - printf($tpl, pht('Email'), '', $create_email); -} - -printf( - $tpl, - pht('Bot'), - $original->getIsSystemAgent() ? 'Y' : 'N', - $set_system_agent ? 'Y' : 'N'); - -if ($verify_email) { - printf( - $tpl, - pht('Verify Email'), - $verify_email->getIsVerified() ? 'Y' : 'N', - $set_verified ? 'Y' : 'N'); -} - -printf( - $tpl, - pht('Admin'), - $original->getIsAdmin() ? 'Y' : 'N', - $set_admin ? 'Y' : 'N'); - -echo "\n"; - -if (!phutil_console_confirm(pht('Save these changes?'), $default_no = false)) { - echo pht('Cancelled.')."\n"; - exit(1); -} - -$user->openTransaction(); - - $editor = new PhabricatorUserEditor(); - - // TODO: This is wrong, but we have a chicken-and-egg problem when you use - // this script to create the first user. - $editor->setActor($user); - - if ($is_new) { - $email = id(new PhabricatorUserEmail()) - ->setAddress($create_email) - ->setIsVerified(1); - - // Unconditionally approve new accounts created from the CLI. - $user->setIsApproved(1); - - $editor->createNewUser($user, $email); - } else { - if ($verify_email) { - $user->setIsEmailVerified(1); - $verify_email->setIsVerified($set_verified ? 1 : 0); - } - $editor->updateUser($user, $verify_email); - } - - $editor->makeSystemAgentUser($user, $set_system_agent); - - $xactions = array(); - $xactions[] = id(new PhabricatorUserTransaction()) - ->setTransactionType( - PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE) - ->setNewValue($set_admin); - - $actor = PhabricatorUser::getOmnipotentUser(); - $content_source = PhabricatorContentSource::newForSource( - PhabricatorConsoleContentSource::SOURCECONST); - - $people_application_phid = id(new PhabricatorPeopleApplication())->getPHID(); - - $transaction_editor = id(new PhabricatorUserTransactionEditor()) - ->setActor($actor) - ->setActingAsPHID($people_application_phid) - ->setContentSource($content_source) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - $transaction_editor->applyTransactions($user, $xactions); - -$user->saveTransaction(); - -echo pht('Saved changes.')."\n"; diff --git a/scripts/user/add_user.php b/scripts/user/add_user.php deleted file mode 100755 index 2554ab3ddc..0000000000 --- a/scripts/user/add_user.php +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env php - '); - exit(1); -} - -$username = $argv[1]; -$email = $argv[2]; -$realname = $argv[3]; -$admin = $argv[4]; - -$admin = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $argv[4]); -if (!$admin) { - throw new Exception( - pht( - 'Admin user must be the username of a valid Phabricator account, used '. - 'to send the new user a welcome email.')); -} - -$existing_user = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $username); -if ($existing_user) { - throw new Exception( - pht( - "There is already a user with the username '%s'!", - $username)); -} - -$existing_email = id(new PhabricatorUserEmail())->loadOneWhere( - 'address = %s', - $email); -if ($existing_email) { - throw new Exception( - pht( - "There is already a user with the email '%s'!", - $email)); -} - -$user = new PhabricatorUser(); -$user->setUsername($username); -$user->setRealname($realname); -$user->setIsApproved(1); - -$email_object = id(new PhabricatorUserEmail()) - ->setAddress($email) - ->setIsVerified(1); - -id(new PhabricatorUserEditor()) - ->setActor($admin) - ->createNewUser($user, $email_object); - -$welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) - ->setSender($admin) - ->setRecipient($user); -if ($welcome_engine->canSendMail()) { - $welcome_engine->sendMail(); -} - -echo pht( - "Created user '%s' (realname='%s', email='%s').\n", - $username, - $realname, - $email); diff --git a/src/docs/user/configuration/configuring_accounts_and_registration.diviner b/src/docs/user/configuration/configuring_accounts_and_registration.diviner index a56d7377cb..e703c46402 100644 --- a/src/docs/user/configuration/configuring_accounts_and_registration.diviner +++ b/src/docs/user/configuration/configuring_accounts_and_registration.diviner @@ -49,30 +49,8 @@ phabricator/ $ ./bin/auth recover ...where `` is the account username you want to recover access to. This will generate a link which will log you in as the specified user. - -Managing Accounts with the Web Console -====================================== - -To manage accounts from the web, login as an administrator account and go to -`/people/` or click "People" on the homepage. Provided you're an admin, -you'll see options to create or edit accounts. - - -Manually Creating New Accounts -============================== - -There are two ways to manually create new accounts: via the web UI using -the "People" application (this is easiest), or via the CLI using the -`accountadmin` binary (this has a few more options). - -To use the CLI script, run: - - phabricator/ $ ./bin/accountadmin - -Some options (like changing certain account flags) are only available from -the CLI. You can also use this script to make a user -an administrator (if you accidentally remove your admin flag) or to create an -administrative account. +For more details on recovering access to accounts and unlocking objects, see +@{article:User Guide: Unlocking Objects}. Next Steps From c4399313737bac4a8f9dda1003ae41ce2432a393 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 21 Aug 2019 17:50:38 -0700 Subject: [PATCH 094/122] Respect "disabled" custom field status granted by "subtype" configuration in form validation Summary: Fixes T13384. Currently, the subtype "disabled" configuration is not respected when selecting fields for `ROLE_EDIT`. The only meaningful caller for `ROLE_EDIT` is transaction validation, but transaction validation should respect fields being disabled by subtype configuration. Test Plan: - Added a "required" Maniphest custom field "F", then "disabled" it in a subtype "S". - Created a task of subtype "S". - Before: Form submission fails with error "F is required", even though the field is not actually visible on the form and can not be set. - After: Form submits cleanly and creates the task. Maniphest Tasks: T13384 Differential Revision: https://secure.phabricator.com/D20726 --- src/infrastructure/customfield/field/PhabricatorCustomField.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index c6c70a9614..9e2bf6895f 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1648,6 +1648,7 @@ abstract class PhabricatorCustomField extends Phobject { $subtype_roles = array( self::ROLE_EDITENGINE, self::ROLE_VIEW, + self::ROLE_EDIT, ); $subtype_roles = array_fuse($subtype_roles); From 5741514aeb5b1eb4b722159154f682a1627463b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 21 Aug 2019 19:11:19 -0700 Subject: [PATCH 095/122] When a client submits an overlong "sourcePath", truncate it and continue Summary: Ref T13385. Currently, if you run `arc diff` in a CWD with more than 255 characters, the workflow fatals against the length of the `sourcePath` database column. In the long term, removing this property is likely desirable. For now, truncate long values and continue. This only meaningfully impacts relatively obscure interactive SVN workflows negatively, and even there, "some arc commands are glitchy in very long working directories in SVN" is still better than "arc diff fatals". Test Plan: - Modified `arc` to submit very long source paths. - Ran `arc diff`. - Before: Fatal when inserting >255 characters into `sourcePath`. - After: Path truncated at 255 bytes. Maniphest Tasks: T13385 Differential Revision: https://secure.phabricator.com/D20727 --- ...DifferentialCreateDiffConduitAPIMethod.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php index 8a0da78865..b0582f8f67 100644 --- a/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php @@ -119,8 +119,11 @@ final class DifferentialCreateDiffConduitAPIMethod break; } + $source_path = $request->getValue('sourcePath'); + $source_path = $this->normalizeSourcePath($source_path); + $diff_data_dict = array( - 'sourcePath' => $request->getValue('sourcePath'), + 'sourcePath' => $source_path, 'sourceMachine' => $request->getValue('sourceMachine'), 'branch' => $request->getValue('branch'), 'creationMethod' => $request->getValue('creationMethod'), @@ -158,4 +161,18 @@ final class DifferentialCreateDiffConduitAPIMethod ); } + private function normalizeSourcePath($source_path) { + // See T13385. This property is probably headed for deletion. Until we get + // there, stop errors arising from running "arc diff" in a working copy + // with too many characters. + + $max_size = id(new DifferentialDiff()) + ->getColumnMaximumByteLength('sourcePath'); + + return id(new PhutilUTF8StringTruncator()) + ->setMaximumBytes($max_size) + ->setTerminator('') + ->truncateString($source_path); + } + } From f1b054a20fd5f1de02a3aaabf0a7cb0a98f11715 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 21 Aug 2019 19:54:24 -0700 Subject: [PATCH 096/122] Correct the interaction between overheating and offset-based paging Summary: Ref T13386. If you issue `differential.query` with a large offset (like 3000), it can overheat regardless of policy filtering and fail with a nonsensical error message. This is because the overheating limit is based only on the query limit, not on the offset. For example, querying for "limit = 100" will never examine more than 1,100 rows, so a query with "limit = 100, offset = 3000" will always fail (provided there are at least that many revisions). Not all numbers work like you might expect them to becuase there's also a 1024-row fetch window, but basically small limits plus big offsets always fail. Test Plan: Artificially reduced the internal window size from 1024 to 5, then ran `differential.query` with `offset=50` and `limit=3`. Before: overheated with weird error message. After: clean result. Maniphest Tasks: T13386 Differential Revision: https://secure.phabricator.com/D20728 --- .../query/policy/PhabricatorPolicyAwareQuery.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 8780584f94..a770c326f9 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -233,7 +233,10 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { // number of records when the viewer can see few or none of them. See // T11773 for some discussion. $this->isOverheated = false; - $overheat_limit = $limit * 10; + + // See T13386. If we on an old offset-based paging workflow, we need + // to base the overheating limit on both the offset and limit. + $overheat_limit = $need * 10; $total_seen = 0; do { From 109d7dcaf197b8b3bdd95f51f46d16e61c0d685b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Aug 2019 07:05:38 -0700 Subject: [PATCH 097/122] Convert "Empower" from state-based MFA to one-shot MFA Summary: Ref T13382. Currently, the "Make Administrator" action in the web UI does state-based MFA. Convert it to one-shot MFA. Test Plan: Empowered and unempowered a user from the web UI, got one-shot MFA'd. Empowered a user from the CLI, no MFA issues. Maniphest Tasks: T13382 Differential Revision: https://secure.phabricator.com/D20729 --- .../controller/PhabricatorPeopleEmpowerController.php | 11 +++-------- .../xaction/PhabricatorUserEmpowerTransaction.php | 7 +++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php index 09021bf73e..22e7c22b68 100644 --- a/src/applications/people/controller/PhabricatorPeopleEmpowerController.php +++ b/src/applications/people/controller/PhabricatorPeopleEmpowerController.php @@ -17,14 +17,8 @@ final class PhabricatorPeopleEmpowerController $done_uri = $this->getApplicationURI("manage/{$id}/"); - id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $done_uri); - $validation_exception = null; - - if ($request->isFormPost()) { + if ($request->isFormOrHisecPost()) { $xactions = array(); $xactions[] = id(new PhabricatorUserTransaction()) ->setTransactionType( @@ -34,7 +28,8 @@ final class PhabricatorPeopleEmpowerController $editor = id(new PhabricatorUserTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) - ->setContinueOnMissingFields(true); + ->setContinueOnMissingFields(true) + ->setCancelURI($done_uri); try { $editor->applyTransactions($user, $xactions); diff --git a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php index 5499f5d8cb..d17418636f 100644 --- a/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserEmpowerTransaction.php @@ -86,4 +86,11 @@ final class PhabricatorUserEmpowerTransaction return null; } + + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return true; + } + } From 353155a2034ed22bf8e5644ddc3eaa70a84e3cd9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Aug 2019 13:04:25 -0700 Subject: [PATCH 098/122] Add "modifiedStart" and "modifiedEnd" constraints to "differential.revision.search" Summary: Fixes T13386. See PHI1391. These constraints largely exist already, but are not yet exposed to Conduit. Also, tweak some keys to support the underlying query. Test Plan: Ran `differential.revision.search` queries with the new constraints. Maniphest Tasks: T13386 Differential Revision: https://secure.phabricator.com/D20730 --- .../query/DifferentialRevisionSearchEngine.php | 18 ++++++++++++++++++ .../storage/DifferentialRevision.php | 8 +++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index 14c9dd0301..2bf196db0c 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -51,6 +51,12 @@ final class DifferentialRevisionSearchEngine $map['createdEnd']); } + if ($map['modifiedStart'] || $map['modifiedEnd']) { + $query->withUpdatedEpochBetween( + $map['modifiedStart'], + $map['modifiedEnd']); + } + return $query; } @@ -100,6 +106,18 @@ final class DifferentialRevisionSearchEngine ->setKey('createdEnd') ->setDescription( pht('Find revisions created at or before a particular time.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Modified After')) + ->setKey('modifiedStart') + ->setIsHidden(true) + ->setDescription( + pht('Find revisions modified at or after a particular time.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Modified Before')) + ->setKey('modifiedEnd') + ->setIsHidden(true) + ->setDescription( + pht('Find revisions modified at or before a particular time.')), ); } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 5e70b52188..c5eaa71b2d 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -112,11 +112,6 @@ final class DifferentialRevision extends DifferentialDAO 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'phid' => array( - 'columns' => array('phid'), - 'unique' => true, - ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), @@ -131,6 +126,9 @@ final class DifferentialRevision extends DifferentialDAO 'key_status' => array( 'columns' => array('status', 'phid'), ), + 'key_modified' => array( + 'columns' => array('dateModified'), + ), ), ) + parent::getConfiguration(); } From ecbc82da33c5dcfc4db3e9e6111cfdccc9c3180d Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Aug 2019 13:29:42 -0700 Subject: [PATCH 099/122] Expose "commits.add|set|remove" on "maniphest.edit" API calls Summary: See PHI1396. Ideally this would be some kind of general-purpose tie-in to object relationships, but see D18456 for precedent. Test Plan: Used `maniphest.edit` to edit associated commits for a task. Differential Revision: https://secure.phabricator.com/D20731 --- .../maniphest/editor/ManiphestEditEngine.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 7cb788e199..fc3c48b205 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -261,6 +261,7 @@ EODOCS $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + $commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST; $src_phid = $object->getPHID(); if ($src_phid) { @@ -270,6 +271,7 @@ EODOCS array( $parent_type, $subtask_type, + $commit_type, )); $edge_query->execute(); @@ -280,9 +282,14 @@ EODOCS $subtask_phids = $edge_query->getDestinationPHIDs( array($src_phid), array($subtask_type)); + + $commit_phids = $edge_query->getDestinationPHIDs( + array($src_phid), + array($commit_type)); } else { $parent_phids = array(); $subtask_phids = array(); + $commit_phids = array(); } $fields[] = id(new PhabricatorHandlesEditField()) @@ -307,7 +314,19 @@ EODOCS ->setIsFormField(false) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $subtask_type) - ->setValue($parent_phids); + ->setValue($subtask_phids); + + $fields[] = id(new PhabricatorHandlesEditField()) + ->setKey('commits') + ->setLabel(pht('Commits')) + ->setDescription(pht('Related commits.')) + ->setConduitDescription(pht('Change the related commits for this task.')) + ->setConduitTypeDescription(pht('List of related commit PHIDs.')) + ->setUseEdgeTransactions(true) + ->setIsFormField(false) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $commit_type) + ->setValue($commit_phids); return $fields; } From 719a7d82c51f58f9c89f9160f5b2e1873d4b37a3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 2 Aug 2019 11:29:45 -0700 Subject: [PATCH 100/122] Refactor the Phortune account detail page into a series of smaller, more focused sections Summary: Ref T13366. Some of the information architecture is a little muddy here, notably an item called "Billing / History" which contains payment methods. Split things up a bit to prepare for adding support for "Email Addresses". Test Plan: {F6676988} Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20697 --- src/__phutil_library_map__.php | 18 ++- .../PhabricatorPhortuneApplication.php | 22 +-- .../PhortuneAccountAddManagerController.php | 8 +- .../PhortuneAccountChargesController.php | 79 +++++++++++ .../PhortuneAccountDetailsController.php | 132 ++++++++++++++++++ ... => PhortuneAccountManagersController.php} | 6 +- .../PhortuneAccountOrdersController.php | 37 +++++ ... => PhortuneAccountOverviewController.php} | 118 ++-------------- ...ortuneAccountPaymentMethodsController.php} | 59 ++------ .../PhortuneAccountProfileController.php | 93 ++++++++++-- .../editor/PhortuneAccountEditEngine.php | 6 +- .../phortune/storage/PhortuneAccount.php | 6 + 12 files changed, 390 insertions(+), 194 deletions(-) create mode 100644 src/applications/phortune/controller/account/PhortuneAccountChargesController.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountDetailsController.php rename src/applications/phortune/controller/account/{PhortuneAccountManagerController.php => PhortuneAccountManagersController.php} (93%) create mode 100644 src/applications/phortune/controller/account/PhortuneAccountOrdersController.php rename src/applications/phortune/controller/account/{PhortuneAccountViewController.php => PhortuneAccountOverviewController.php} (51%) rename src/applications/phortune/controller/account/{PhortuneAccountBillingController.php => PhortuneAccountPaymentMethodsController.php} (65%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6f36855761..100f7fc30a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5221,25 +5221,28 @@ phutil_register_library_map(array( 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', 'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php', - 'PhortuneAccountBillingController' => 'applications/phortune/controller/account/PhortuneAccountBillingController.php', 'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php', 'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php', + 'PhortuneAccountChargesController' => 'applications/phortune/controller/account/PhortuneAccountChargesController.php', 'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php', + 'PhortuneAccountDetailsController' => 'applications/phortune/controller/account/PhortuneAccountDetailsController.php', 'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php', 'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', 'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php', 'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php', - 'PhortuneAccountManagerController' => 'applications/phortune/controller/account/PhortuneAccountManagerController.php', + 'PhortuneAccountManagersController' => 'applications/phortune/controller/account/PhortuneAccountManagersController.php', 'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php', + 'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php', + 'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', + 'PhortuneAccountPaymentMethodsController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php', 'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php', - 'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php', 'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php', 'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php', 'PhortuneAddPaymentMethodAction' => 'applications/phortune/action/PhortuneAddPaymentMethodAction.php', @@ -11753,25 +11756,28 @@ phutil_register_library_map(array( ), 'PhortuneAccountAddManagerController' => 'PhortuneController', 'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType', - 'PhortuneAccountBillingController' => 'PhortuneAccountProfileController', 'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType', 'PhortuneAccountChargeListController' => 'PhortuneController', + 'PhortuneAccountChargesController' => 'PhortuneAccountProfileController', 'PhortuneAccountController' => 'PhortuneController', + 'PhortuneAccountDetailsController' => 'PhortuneAccountProfileController', 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditEngine' => 'PhabricatorEditEngine', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneAccountListController' => 'PhortuneController', - 'PhortuneAccountManagerController' => 'PhortuneAccountProfileController', + 'PhortuneAccountManagersController' => 'PhortuneAccountProfileController', 'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType', + 'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController', + 'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', + 'PhortuneAccountPaymentMethodsController' => 'PhortuneAccountProfileController', 'PhortuneAccountProfileController' => 'PhortuneAccountController', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController', 'PhortuneAccountTransaction' => 'PhabricatorModularTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType', - 'PhortuneAccountViewController' => 'PhortuneAccountProfileController', 'PhortuneAdHocCart' => 'PhortuneCartImplementation', 'PhortuneAdHocProduct' => 'PhortuneProductImplementation', 'PhortuneAddPaymentMethodAction' => 'PhabricatorSystemAction', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 0ffc138f65..de3c740e2b 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -35,7 +35,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '/phortune/' => array( '' => 'PhortuneLandingController', '(?P\d+)/' => array( - '' => 'PhortuneAccountViewController', + '' => 'PhortuneAccountOverviewController', 'card/' => array( 'new/' => 'PhortunePaymentMethodCreateController', ), @@ -69,15 +69,17 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '' => 'PhortuneAccountListController', $this->getEditRoutePattern('edit/') => 'PhortuneAccountEditController', - 'edit/(?:(?P\d+)/)?' => 'PhortuneAccountEditController', - 'add/manager/(?:(?P\d+)/)?' - => 'PhortuneAccountAddManagerController', - 'billing/(?:(?P\d+)/)?' => 'PhortuneAccountBillingController', - 'subscription/(?:(?P\d+)/)?' - => 'PhortuneAccountSubscriptionController', - 'manager/' => array( - '(?:(?P\d+)/)?' => 'PhortuneAccountManagerController', - 'add/(?:(?P\d+)/)?' => 'PhortuneAccountAddManagerController', + + '(?P\d+)/' => array( + 'details/' => 'PhortuneAccountDetailsController', + 'methods/' => 'PhortuneAccountPaymentMethodsController', + 'orders/' => 'PhortuneAccountOrdersController', + 'charges/' => 'PhortuneAccountChargesController', + 'subscriptions/' => 'PhortuneAccountSubscriptionController', + 'managers/' => array( + '' => 'PhortuneAccountManagersController', + 'add/' => 'PhortuneAccountAddManagerController', + ), ), ), 'product/' => array( diff --git a/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php b/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php index 34bb0a480b..0c8c71d968 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php @@ -4,7 +4,7 @@ final class PhortuneAccountAddManagerController extends PhortuneController { public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); + $id = $request->getURIData('accountID'); $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) @@ -21,7 +21,7 @@ final class PhortuneAccountAddManagerController extends PhortuneController { $v_managers = array(); $e_managers = null; - $account_uri = $this->getApplicationURI("/account/manager/{$id}/"); + $account_uri = $this->getApplicationURI("/account/{$id}/managers/"); if ($request->isFormPost()) { $xactions = array(); @@ -64,11 +64,11 @@ final class PhortuneAccountAddManagerController extends PhortuneController { ->setError($e_managers)); return $this->newDialog() - ->setTitle(pht('Add New Manager')) + ->setTitle(pht('Add New Managers')) ->appendForm($form) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addCancelButton($account_uri) - ->addSubmitButton(pht('Add Manager')); + ->addSubmitButton(pht('Add Managers')); } diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php new file mode 100644 index 0000000000..6fc0209f94 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php @@ -0,0 +1,79 @@ +loadAccount(); + if ($response) { + return $response; + } + + $account = $this->getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Order History')); + + $header = $this->buildHeaderView(); + $charge_history = $this->buildChargeHistorySection($account); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $charge_history, + )); + + $navigation = $this->buildSideNavView('charges'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildChargeHistorySection(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $charges = id(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needCarts(true) + ->setLimit(100) + ->execute(); + + $phids = array(); + foreach ($charges as $charge) { + $phids[] = $charge->getProviderPHID(); + $phids[] = $charge->getCartPHID(); + $phids[] = $charge->getMerchantPHID(); + $phids[] = $charge->getPaymentMethodPHID(); + } + + $handles = $this->loadViewerHandles($phids); + + $charges_uri = $this->getApplicationURI($account->getID().'/charge/'); + + $table = id(new PhortuneChargeTableView()) + ->setUser($viewer) + ->setCharges($charges) + ->setHandles($handles); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Charges')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($charges_uri) + ->setText(pht('View All Charges'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php new file mode 100644 index 0000000000..476c802597 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php @@ -0,0 +1,132 @@ +loadAccount(); + if ($response) { + return $response; + } + + $account = $this->getAccount(); + $title = $account->getName(); + + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->setBorder(true); + + $header = $this->buildHeaderView(); + + $details = $this->newDetailsView($account); + + $curtain = $this->buildCurtainView($account); + + $timeline = $this->buildTransactionTimeline( + $account, + new PhortuneAccountTransactionQuery()); + $timeline->setShouldTerminate(true); + + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $timeline, + )); + + $navigation = $this->buildSideNavView('details'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + + } + + private function buildCurtainView(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI('account/edit/'.$account->getID().'/'); + + $curtain = $this->newCurtainView($account); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Account')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $member_phids = $account->getMemberPHIDs(); + $handles = $viewer->loadHandles($member_phids); + + $member_list = id(new PHUIObjectItemListView()) + ->setSimple(true); + + foreach ($member_phids as $member_phid) { + $image_uri = $handles[$member_phid]->getImageURI(); + $image_href = $handles[$member_phid]->getURI(); + $person = $handles[$member_phid]; + + $member = id(new PHUIObjectItemView()) + ->setImageURI($image_uri) + ->setHref($image_href) + ->setHeader($person->getFullName()); + + $member_list->addItem($member); + } + + $curtain->newPanel() + ->setHeaderText(pht('Managers')) + ->appendChild($member_list); + + return $curtain; + } + + private function newDetailsView(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $view->addProperty(pht('Account Name'), $account->getName()); + + $display_name = $account->getBillingName(); + if (!strlen($display_name)) { + $display_name = phutil_tag('em', array(), pht('None')); + } + + $display_address = $account->getBillingAddress(); + if (!strlen($display_address)) { + $display_address = phutil_tag('em', array(), pht('None')); + } else { + $display_address = phutil_escape_html_newlines($display_address); + } + + $view->addProperty(pht('Billing Name'), $display_name); + $view->addProperty(pht('Billing Address'), $display_address); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Account Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountManagerController.php b/src/applications/phortune/controller/account/PhortuneAccountManagersController.php similarity index 93% rename from src/applications/phortune/controller/account/PhortuneAccountManagerController.php rename to src/applications/phortune/controller/account/PhortuneAccountManagersController.php index 502fbfe52e..f79d15a7f3 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountManagerController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountManagersController.php @@ -1,6 +1,6 @@ setTag('a') - ->setText(pht('New Manager')) + ->setText(pht('Add Managers')) ->setIcon('fa-plus') ->setWorkflow(true) ->setDisabled(!$can_edit) - ->setHref("/phortune/account/manager/add/{$id}/"); + ->setHref("/phortune/account/{$id}/managers/add/"); $header = id(new PHUIHeaderView()) ->setHeader(pht('Account Managers')) diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php new file mode 100644 index 0000000000..38327a7099 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php @@ -0,0 +1,37 @@ +loadAccount(); + if ($response) { + return $response; + } + + $account = $this->getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Order History')); + + $header = $this->buildHeaderView(); + $order_history = $this->newRecentOrdersView($account, 100); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $order_history, + )); + + $navigation = $this->buildSideNavView('orders'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountViewController.php b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php similarity index 51% rename from src/applications/phortune/controller/account/PhortuneAccountViewController.php rename to src/applications/phortune/controller/account/PhortuneAccountOverviewController.php index 6537577921..52b8127ab3 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php @@ -1,6 +1,6 @@ buildHeaderView(); - $curtain = $this->buildCurtainView($account); $status = $this->buildStatusView($account, $invoices); $invoices = $this->buildInvoicesSection($account, $invoices); - $purchase_history = $this->buildPurchaseHistorySection($account); - - $timeline = $this->buildTransactionTimeline( - $account, - new PhortuneAccountTransactionQuery()); - $timeline->setShouldTerminate(true); + $purchase_history = $this->newRecentOrdersView($account, 10); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $status, - $invoices, - $purchase_history, - $timeline, - )); + ->setFooter( + array( + $status, + $invoices, + $purchase_history, + )); $navigation = $this->buildSideNavView('overview'); @@ -53,7 +46,6 @@ final class PhortuneAccountViewController ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); - } private function buildStatusView(PhortuneAccount $account, $invoices) { @@ -67,51 +59,6 @@ final class PhortuneAccountViewController return $view; } - private function buildCurtainView(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - - $edit_uri = $this->getApplicationURI('account/edit/'.$account->getID().'/'); - - $curtain = $this->newCurtainView($account); - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Account')) - ->setIcon('fa-pencil') - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $member_phids = $account->getMemberPHIDs(); - $handles = $viewer->loadHandles($member_phids); - - $member_list = id(new PHUIObjectItemListView()) - ->setSimple(true); - - foreach ($member_phids as $member_phid) { - $image_uri = $handles[$member_phid]->getImageURI(); - $image_href = $handles[$member_phid]->getURI(); - $person = $handles[$member_phid]; - - $member = id(new PHUIObjectItemView()) - ->setImageURI($image_uri) - ->setHref($image_href) - ->setHeader($person->getFullName()); - - $member_list->addItem($member); - } - - $curtain->newPanel() - ->setHeaderText(pht('Managers')) - ->appendChild($member_list); - - return $curtain; - } - private function buildInvoicesSection( PhortuneAccount $account, array $carts) { @@ -144,55 +91,6 @@ final class PhortuneAccountViewController ->setTable($table); } - private function buildPurchaseHistorySection(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $carts = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->needPurchases(true) - ->withStatuses( - array( - PhortuneCart::STATUS_PURCHASING, - PhortuneCart::STATUS_CHARGED, - PhortuneCart::STATUS_HOLD, - PhortuneCart::STATUS_REVIEW, - PhortuneCart::STATUS_PURCHASED, - )) - ->setLimit(10) - ->execute(); - - $phids = array(); - foreach ($carts as $cart) { - $phids[] = $cart->getPHID(); - foreach ($cart->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $orders_uri = $this->getApplicationURI($account->getID().'/order/'); - - $table = id(new PhortuneOrderTableView()) - ->setUser($viewer) - ->setCarts($carts) - ->setHandles($handles); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Recent Orders')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-list') - ->setHref($orders_uri) - ->setText(pht('View All Orders'))); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); - } - protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); diff --git a/src/applications/phortune/controller/account/PhortuneAccountBillingController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php similarity index 65% rename from src/applications/phortune/controller/account/PhortuneAccountBillingController.php rename to src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php index 0660358af7..519199fc92 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountBillingController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php @@ -1,6 +1,6 @@ getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Billing')); + $crumbs->addTextCrumb(pht('Payment Methods')); $header = $this->buildHeaderView(); $methods = $this->buildPaymentMethodsSection($account); - $charge_history = $this->buildChargeHistorySection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $methods, - $charge_history, - )); + ->setFooter( + array( + $methods, + )); - $navigation = $this->buildSideNavView('billing'); + $navigation = $this->buildSideNavView('methods'); return $this->newPage() ->setTitle($title) @@ -60,7 +59,7 @@ final class PhortuneAccountBillingController ->setUser($viewer) ->setFlush(true) ->setNoDataString( - pht('No payment methods associated with this account.')); + pht('There are no payment methods associated with this account.')); $methods = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) @@ -116,46 +115,4 @@ final class PhortuneAccountBillingController ->setObjectList($list); } - private function buildChargeHistorySection(PhortuneAccount $account) { - $viewer = $this->getViewer(); - - $charges = id(new PhortuneChargeQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->needCarts(true) - ->setLimit(10) - ->execute(); - - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - - $handles = $this->loadViewerHandles($phids); - - $charges_uri = $this->getApplicationURI($account->getID().'/charge/'); - - $table = id(new PhortuneChargeTableView()) - ->setUser($viewer) - ->setCharges($charges) - ->setHandles($handles); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Charge History')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-list') - ->setHref($charges_uri) - ->setText(pht('View All Charges'))); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); - } - } diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 2a5448d5cc..06934bb00d 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -43,21 +43,43 @@ abstract class PhortuneAccountProfileController 'fa-user-circle'); $nav->addFilter( - 'subscriptions', - pht('Subscriptions'), - $this->getApplicationURI("/account/subscription/{$id}/"), - 'fa-retweet'); + 'details', + pht('Account Details'), + $this->getApplicationURI("/account/{$id}/details/"), + 'fa-address-card-o'); + + $nav->addLabel(pht('Payments')); $nav->addFilter( - 'billing', - pht('Billing / History'), - $this->getApplicationURI("/account/billing/{$id}/"), + 'methods', + pht('Payment Methods'), + $this->getApplicationURI("/account/{$id}/methods/"), 'fa-credit-card'); + $nav->addFilter( + 'subscriptions', + pht('Subscriptions'), + $this->getApplicationURI("/account/{$id}/subscriptions/"), + 'fa-retweet'); + + $nav->addFilter( + 'orders', + pht('Order History'), + $this->getApplicationURI("/account/{$id}/orders/"), + 'fa-shopping-bag'); + + $nav->addFilter( + 'charges', + pht('Charge History'), + $this->getApplicationURI("/account/{$id}/charges/"), + 'fa-calculator'); + + $nav->addLabel(pht('Personnel')); + $nav->addFilter( 'managers', - pht('Managers'), - $this->getApplicationURI("/account/manager/{$id}/"), + pht('Account Managers'), + $this->getApplicationURI("/account/{$id}/managers/"), 'fa-group'); $nav->selectFilter($filter); @@ -65,4 +87,57 @@ abstract class PhortuneAccountProfileController return $nav; } + final protected function newRecentOrdersView( + PhortuneAccount $account, + $limit) { + + $viewer = $this->getViewer(); + + $carts = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withStatuses( + array( + PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, + PhortuneCart::STATUS_PURCHASED, + )) + ->setLimit($limit) + ->execute(); + + $phids = array(); + foreach ($carts as $cart) { + $phids[] = $cart->getPHID(); + foreach ($cart->getPurchases() as $purchase) { + $phids[] = $purchase->getPHID(); + } + } + $handles = $this->loadViewerHandles($phids); + + $orders_uri = $this->getApplicationURI($account->getID().'/order/'); + + $table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($carts) + ->setHandles($handles); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Orders')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($orders_uri) + ->setText(pht('View All Orders'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + } diff --git a/src/applications/phortune/editor/PhortuneAccountEditEngine.php b/src/applications/phortune/editor/PhortuneAccountEditEngine.php index 1b6f9a5040..d7272965c2 100644 --- a/src/applications/phortune/editor/PhortuneAccountEditEngine.php +++ b/src/applications/phortune/editor/PhortuneAccountEditEngine.php @@ -62,7 +62,11 @@ final class PhortuneAccountEditEngine } protected function getObjectViewURI($object) { - return $object->getURI(); + if ($this->getIsCreate()) { + return $object->getURI(); + } else { + return $object->getDetailsURI(); + } } protected function buildCustomEditFields($object) { diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index ade98d327f..2fb2a40589 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -103,6 +103,12 @@ final class PhortuneAccount extends PhortuneDAO return '/phortune/'.$this->getID().'/'; } + public function getDetailsURI() { + return urisprintf( + '/phortune/account/%d/details/', + $this->getID()); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ From e3ba53078e1be289601f53799c219543744f90a0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 2 Aug 2019 12:14:58 -0700 Subject: [PATCH 101/122] Add scaffolding for ad-hoc email addresses associated with Phortune accounts Summary: Depends on D20697. Ref T8389. Add support for adding "billing@enterprise.com" and similar to Phortune accounts. Test Plan: Added and edited email addresses for a payment account. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T8389 Differential Revision: https://secure.phabricator.com/D20713 --- .../autopatches/20190802.email.01.storage.sql | 12 ++ .../autopatches/20190802.email.02.xaction.sql | 19 +++ src/__phutil_library_map__.php | 31 +++++ .../PhabricatorPhortuneApplication.php | 10 ++ .../constants/PhortuneAccountEmailStatus.php | 31 +++++ .../account/PhortuneAccountController.php | 6 +- ...hortuneAccountEmailAddressesController.php | 90 +++++++++++++ .../PhortuneAccountEmailEditController.php | 28 ++++ .../PhortuneAccountEmailViewController.php | 93 ++++++++++++++ .../PhortuneAccountProfileController.php | 6 + .../editor/PhortuneAccountEmailEditEngine.php | 114 +++++++++++++++++ .../editor/PhortuneAccountEmailEditor.php | 36 ++++++ .../phid/PhortuneAccountEmailPHIDType.php | 41 ++++++ .../query/PhortuneAccountEmailQuery.php | 91 +++++++++++++ .../PhortuneAccountEmailTransactionQuery.php | 10 ++ .../phortune/storage/PhortuneAccount.php | 6 + .../phortune/storage/PhortuneAccountEmail.php | 121 ++++++++++++++++++ .../PhortuneAccountEmailTransaction.php | 18 +++ ...PhortuneAccountEmailAddressTransaction.php | 63 +++++++++ .../PhortuneAccountEmailTransactionType.php | 4 + 20 files changed, 829 insertions(+), 1 deletion(-) create mode 100644 resources/sql/autopatches/20190802.email.01.storage.sql create mode 100644 resources/sql/autopatches/20190802.email.02.xaction.sql create mode 100644 src/applications/phortune/constants/PhortuneAccountEmailStatus.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php create mode 100644 src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php create mode 100644 src/applications/phortune/editor/PhortuneAccountEmailEditor.php create mode 100644 src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php create mode 100644 src/applications/phortune/query/PhortuneAccountEmailQuery.php create mode 100644 src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php create mode 100644 src/applications/phortune/storage/PhortuneAccountEmail.php create mode 100644 src/applications/phortune/storage/PhortuneAccountEmailTransaction.php create mode 100644 src/applications/phortune/xaction/PhortuneAccountEmailAddressTransaction.php create mode 100644 src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php diff --git a/resources/sql/autopatches/20190802.email.01.storage.sql b/resources/sql/autopatches/20190802.email.01.storage.sql new file mode 100644 index 0000000000..f362067e6d --- /dev/null +++ b/resources/sql/autopatches/20190802.email.01.storage.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_accountemail ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + accountPHID VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + address VARCHAR(128) NOT NULL COLLATE {$COLLATE_SORT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + addressKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + accessKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190802.email.02.xaction.sql b/resources/sql/autopatches/20190802.email.02.xaction.sql new file mode 100644 index 0000000000..d65f8d8b32 --- /dev/null +++ b/resources/sql/autopatches/20190802.email.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_accountemailtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 100f7fc30a..89861a8e42 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5229,6 +5229,19 @@ phutil_register_library_map(array( 'PhortuneAccountEditController' => 'applications/phortune/controller/account/PhortuneAccountEditController.php', 'PhortuneAccountEditEngine' => 'applications/phortune/editor/PhortuneAccountEditEngine.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', + 'PhortuneAccountEmail' => 'applications/phortune/storage/PhortuneAccountEmail.php', + 'PhortuneAccountEmailAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailAddressTransaction.php', + 'PhortuneAccountEmailAddressesController' => 'applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php', + 'PhortuneAccountEmailEditController' => 'applications/phortune/controller/account/PhortuneAccountEmailEditController.php', + 'PhortuneAccountEmailEditEngine' => 'applications/phortune/editor/PhortuneAccountEmailEditEngine.php', + 'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php', + 'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php', + 'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php', + 'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php', + 'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php', + 'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php', + 'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php', + 'PhortuneAccountEmailViewController' => 'applications/phortune/controller/account/PhortuneAccountEmailViewController.php', 'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php', 'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php', 'PhortuneAccountManagersController' => 'applications/phortune/controller/account/PhortuneAccountManagersController.php', @@ -11764,6 +11777,24 @@ phutil_register_library_map(array( 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditEngine' => 'PhabricatorEditEngine', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneAccountEmail' => array( + 'PhortuneDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + ), + 'PhortuneAccountEmailAddressTransaction' => 'PhortuneAccountEmailTransactionType', + 'PhortuneAccountEmailAddressesController' => 'PhortuneAccountProfileController', + 'PhortuneAccountEmailEditController' => 'PhortuneAccountController', + 'PhortuneAccountEmailEditEngine' => 'PhabricatorEditEngine', + 'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType', + 'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneAccountEmailStatus' => 'Phobject', + 'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction', + 'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType', + 'PhortuneAccountEmailViewController' => 'PhortuneAccountController', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneAccountListController' => 'PhortuneController', 'PhortuneAccountManagersController' => 'PhortuneAccountProfileController', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index de3c740e2b..fad3d19c66 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -80,8 +80,18 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '' => 'PhortuneAccountManagersController', 'add/' => 'PhortuneAccountAddManagerController', ), + 'addresses/' => array( + '' => 'PhortuneAccountEmailAddressesController', + $this->getEditRoutePattern('edit/') + => 'PhortuneAccountEmailEditController', + ), ), ), + 'address/' => array( + '(?P\d+)/' => 'PhortuneAccountEmailViewController', + $this->getEditRoutePattern('edit/') + => 'PhortuneAccountEmailEditController', + ), 'product/' => array( '' => 'PhortuneProductListController', 'view/(?P\d+)/' => 'PhortuneProductViewController', diff --git a/src/applications/phortune/constants/PhortuneAccountEmailStatus.php b/src/applications/phortune/constants/PhortuneAccountEmailStatus.php new file mode 100644 index 0000000000..fa18c3f466 --- /dev/null +++ b/src/applications/phortune/constants/PhortuneAccountEmailStatus.php @@ -0,0 +1,31 @@ + array( + 'name' => pht('Active'), + 'closed' => false, + ), + self::STATUS_DISABLED => array( + 'name' => pht('Disabled'), + 'closed' => true, + ), + self::STATUS_UNSUBSCRIBED => array( + 'name' => pht('Unsubscribed'), + 'closed' => true, + ), + ); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountController.php b/src/applications/phortune/controller/account/PhortuneAccountController.php index 2ba3b393a2..a7361254e2 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountController.php @@ -9,6 +9,11 @@ abstract class PhortuneAccountController return $this->account; } + protected function setAccount(PhortuneAccount $account) { + $this->account = $account; + return $this; + } + protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); @@ -29,7 +34,6 @@ abstract class PhortuneAccountController return $this->loadAccountForEdit(); } - protected function loadAccountForEdit() { $viewer = $this->getViewer(); $request = $this->getRequest(); diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php new file mode 100644 index 0000000000..1abff653f1 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php @@ -0,0 +1,90 @@ +loadAccount(); + if ($response) { + return $response; + } + + $account = $this->getAccount(); + $title = $account->getName(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Email Addresses')); + + $header = $this->buildHeaderView(); + $addresses = $this->buildAddressesSection($account); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $addresses, + )); + + $navigation = $this->buildSideNavView('addresses'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildAddressesSection(PhortuneAccount $account) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $id = $account->getID(); + + $add = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Add Address')) + ->setIcon('fa-plus') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit) + ->setHref("/phortune/account/{$id}/addresses/edit/"); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Billing Email Addresses')) + ->addActionLink($add); + + $addresses = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->execute(); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setNoDataString( + pht( + 'There are no billing email addresses associated '. + 'with this account.')); + + $addresses = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->execute(); + foreach ($addresses as $address) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($address->getObjectName()) + ->setHeader($address->getAddress()) + ->setHref($address->getURI()); + + $list->addItem($item); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($list); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php new file mode 100644 index 0000000000..0257d574dd --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php @@ -0,0 +1,28 @@ +setController($this); + + if (!$request->getURIData('id')) { + + if (!$request->getURIData('accountID')) { + return new Aphront404Response(); + } + + $response = $this->loadAccount(); + if ($response) { + return $response; + } + + $account = $this->getAccount(); + + $engine->setAccount($account); + } + + return $engine->buildResponse(); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php new file mode 100644 index 0000000000..c9cb2570c3 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -0,0 +1,93 @@ +getViewer(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $account = $address->getAccount(); + $this->setAccount($account); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Email Addresses'), $account->getEmailAddressesURI()) + ->addTextCrumb($address->getObjectName()) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Account Email: %s', $address->getAddress())); + + $details = $this->newDetailsView($address); + + $timeline = $this->buildTransactionTimeline( + $address, + new PhortuneAccountEmailTransactionQuery()); + $timeline->setShouldTerminate(true); + + $curtain = $this->buildCurtainView($address); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $timeline, + )); + + return $this->newPage() + ->setTitle($address->getObjectName()) + ->setCrumbs($crumbs) + ->appendChild($view); + + } + + private function buildCurtainView(PhortuneAccountEmail $address) { + $viewer = $this->getViewer(); + $account = $address->getAccount(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $address, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'address/edit/%d/', + $address->getID())); + + $curtain = $this->newCurtainView($account); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Address')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + + private function newDetailsView(PhortuneAccountEmail $address) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $view->addProperty(pht('Email Address'), $address->getAddress()); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Email Address Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 06934bb00d..9de7b0a8de 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -82,6 +82,12 @@ abstract class PhortuneAccountProfileController $this->getApplicationURI("/account/{$id}/managers/"), 'fa-group'); + $nav->addFilter( + 'addresses', + pht('Email Addresses'), + $this->getApplicationURI("/account/{$id}/addresses/"), + 'fa-envelope-o'); + $nav->selectFilter($filter); return $nav; diff --git a/src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php b/src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php new file mode 100644 index 0000000000..c732e4215f --- /dev/null +++ b/src/applications/phortune/editor/PhortuneAccountEmailEditEngine.php @@ -0,0 +1,114 @@ +account = $account; + return $this; + } + + public function getAccount() { + return $this->account; + } + + public function getEngineName() { + return pht('Phortune Account Emails'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorPhortuneApplication'; + } + + public function getSummaryHeader() { + return pht('Configure Phortune Account Email Forms'); + } + + public function getSummaryText() { + return pht( + 'Configure creation and editing forms for Phortune Account '. + 'Email Addresses.'); + } + + public function isEngineConfigurable() { + return false; + } + + protected function newEditableObject() { + $viewer = $this->getViewer(); + + $account = $this->getAccount(); + if (!$account) { + $account = new PhortuneAccount(); + } + + return PhortuneAccountEmail::initializeNewAddress( + $account, + $viewer->getPHID()); + } + + protected function newObjectQuery() { + return new PhortuneAccountEmailQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Add Email Address'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Account Email: %s', $object->getAddress()); + } + + protected function getObjectEditShortText($object) { + return pht('%s', $object->getAddress()); + } + + protected function getObjectCreateShortText() { + return pht('Add Email Address'); + } + + protected function getObjectName() { + return pht('Account Email'); + } + + protected function getObjectCreateCancelURI($object) { + return $this->getAccount()->getEmailAddressesURI(); + } + + protected function getEditorURI() { + return $this->getApplication()->getApplicationURI('address/edit/'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + + if ($this->getIsCreate()) { + $address_field = id(new PhabricatorTextEditField()) + ->setTransactionType( + PhortuneAccountEmailAddressTransaction::TRANSACTIONTYPE) + ->setIsRequired(true); + } else { + $address_field = new PhabricatorStaticEditField(); + } + + $address_field + ->setKey('address') + ->setLabel(pht('Email Address')) + ->setDescription(pht('Email address.')) + ->setConduitTypeDescription(pht('New email address.')) + ->setValue($object->getAddress()); + + return array( + $address_field, + ); + } + +} diff --git a/src/applications/phortune/editor/PhortuneAccountEmailEditor.php b/src/applications/phortune/editor/PhortuneAccountEmailEditor.php new file mode 100644 index 0000000000..40d12a97ba --- /dev/null +++ b/src/applications/phortune/editor/PhortuneAccountEmailEditor.php @@ -0,0 +1,36 @@ +getAddress()), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + +} diff --git a/src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php b/src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php new file mode 100644 index 0000000000..fccd50cf16 --- /dev/null +++ b/src/applications/phortune/phid/PhortuneAccountEmailPHIDType.php @@ -0,0 +1,41 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $email = $objects[$phid]; + + $id = $email->getID(); + + $handle->setName($email->getObjectName()); + } + } + +} diff --git a/src/applications/phortune/query/PhortuneAccountEmailQuery.php b/src/applications/phortune/query/PhortuneAccountEmailQuery.php new file mode 100644 index 0000000000..4494372ef6 --- /dev/null +++ b/src/applications/phortune/query/PhortuneAccountEmailQuery.php @@ -0,0 +1,91 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withAccountPHIDs(array $phids) { + $this->accountPHIDs = $phids; + return $this; + } + + public function newResultObject() { + return new PhortuneAccountEmail(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $addresses) { + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs(mpull($addresses, 'getAccountPHID')) + ->execute(); + $accounts = mpull($accounts, null, 'getPHID'); + + foreach ($addresses as $key => $address) { + $account = idx($accounts, $address->getAccountPHID()); + + if (!$account) { + $this->didRejectResult($addresses[$key]); + unset($addresses[$key]); + continue; + } + + $address->attachAccount($account); + } + + return $addresses; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'address.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'address.phid IN (%Ls)', + $this->phids); + } + + if ($this->accountPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'address.accountPHID IN (%Ls)', + $this->accountPHIDs); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorPhortuneApplication'; + } + + protected function getPrimaryTableAlias() { + return 'address'; + } + +} diff --git a/src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php b/src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php new file mode 100644 index 0000000000..2aa9d8418e --- /dev/null +++ b/src/applications/phortune/query/PhortuneAccountEmailTransactionQuery.php @@ -0,0 +1,10 @@ +getID()); } + public function getEmailAddressesURI() { + return urisprintf( + '/phortune/account/%d/addresses/', + $this->getID()); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php new file mode 100644 index 0000000000..674a86be4b --- /dev/null +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -0,0 +1,121 @@ + true, + self::CONFIG_COLUMN_SCHEMA => array( + 'address' => 'sort128', + 'status' => 'text32', + 'addressKey' => 'text32', + 'accessKey' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_account' => array( + 'columns' => array('accountPHID', 'address'), + 'unique' => true, + ), + 'key_address' => array( + 'columns' => array('addressKey'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhortuneAccountEmailPHIDType::TYPECONST; + } + + public static function initializeNewAddress( + PhortuneAccount $account, + $author_phid) { + + $address_key = Filesystem::readRandomCharacters(16); + $access_key = Filesystem::readRandomCharacters(16); + $default_status = PhortuneAccountEmailStatus::getDefaultStatusConstant(); + + return id(new self()) + ->setAuthorPHID($author_phid) + ->setAccountPHID($account->getPHID()) + ->setStatus($default_status) + ->attachAccount($account) + ->setAddressKey($address_key) + ->setAccessKey($access_key); + } + + public function attachAccount(PhortuneAccount $account) { + $this->account = $account; + return $this; + } + + public function getAccount() { + return $this->assertAttached($this->account); + } + + public function getObjectName() { + return pht('Account Email %d', $this->getID()); + } + + public function getURI() { + return urisprintf( + '/phortune/address/%d/', + $this->getID()); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + return array( + array($this->getAccount(), $capability), + ); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhortuneAccountEmailEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhortuneAccountEmailTransaction(); + } + +} diff --git a/src/applications/phortune/storage/PhortuneAccountEmailTransaction.php b/src/applications/phortune/storage/PhortuneAccountEmailTransaction.php new file mode 100644 index 0000000000..699c209438 --- /dev/null +++ b/src/applications/phortune/storage/PhortuneAccountEmailTransaction.php @@ -0,0 +1,18 @@ +getAddress(); + } + + public function applyInternalEffects($object, $value) { + $object->setAddress($value); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getAddress(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('You must provide an email address.')); + } + + $max_length = $object->getColumnMaximumByteLength('address'); + foreach ($xactions as $xaction) { + $old_value = $xaction->getOldValue(); + $new_value = $xaction->getNewValue(); + + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'The address can be no longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + continue; + } + + if (!PhabricatorUserEmail::isValidAddress($new_value)) { + $errors[] = $this->newInvalidError( + PhabricatorUserEmail::describeValidAddresses(), + $xaction); + continue; + } + + if ($new_value !== $old_value) { + if (!$this->isNewObject()) { + $errors[] = $this->newInvalidError( + pht( + 'Account email addresses can not be edited once they are '. + 'created. To change the billing address for an account, '. + 'disable the old address and then add a new address.'), + $xaction); + continue; + } + } + + } + + return $errors; + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php b/src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php new file mode 100644 index 0000000000..2654796207 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailTransactionType.php @@ -0,0 +1,4 @@ + Date: Thu, 15 Aug 2019 16:06:56 -0700 Subject: [PATCH 102/122] In Phortune, write relationships between payment accounts and merchants they interact with Summary: Depends on D20713. Ref T13366. When a payment account establishes a relationship with a merchant by creating a cart or subscription, create an edge to give the merchant access to view the payment account. Also, migrate all existing subscriptions and carts to write these edges. This aims at straightening out Phortune permissions, which are currently a bit wonky on a couple of dimensions. See T13366 for detailed discussion. Test Plan: - Created and edited carts/subscriptions, saw edges write. - Ran migrations, saw edges write. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20715 --- .../autopatches/20190815.account.01.carts.php | 10 ++++++++ .../20190815.account.02.subscriptions.php | 10 ++++++++ src/__phutil_library_map__.php | 4 +++ .../PhortuneAccountDetailsController.php | 22 ++++++++++++++++ .../PhortuneAccountHasMerchantEdgeType.php | 11 ++++++++ .../PhortuneMerchantHasAccountEdgeType.php | 12 +++++++++ .../phortune/editor/PhortuneCartEditor.php | 12 +++++++++ .../phid/PhortuneMerchantPHIDType.php | 11 ++++---- .../phortune/query/PhortuneAccountQuery.php | 25 +++++++++++++++++-- .../phortune/storage/PhortuneAccount.php | 22 ++++++++++++++++ .../phortune/storage/PhortuneSubscription.php | 4 +++ 11 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 resources/sql/autopatches/20190815.account.01.carts.php create mode 100644 resources/sql/autopatches/20190815.account.02.subscriptions.php create mode 100644 src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php create mode 100644 src/applications/phortune/edge/PhortuneMerchantHasAccountEdgeType.php diff --git a/resources/sql/autopatches/20190815.account.01.carts.php b/resources/sql/autopatches/20190815.account.01.carts.php new file mode 100644 index 0000000000..2332dd642f --- /dev/null +++ b/resources/sql/autopatches/20190815.account.01.carts.php @@ -0,0 +1,10 @@ +addEdge($cart->getAccountPHID(), $edge_type, $cart->getMerchantPHID()) + ->save(); +} diff --git a/resources/sql/autopatches/20190815.account.02.subscriptions.php b/resources/sql/autopatches/20190815.account.02.subscriptions.php new file mode 100644 index 0000000000..38db05b0ef --- /dev/null +++ b/resources/sql/autopatches/20190815.account.02.subscriptions.php @@ -0,0 +1,10 @@ +addEdge($sub->getAccountPHID(), $edge_type, $sub->getMerchantPHID()) + ->save(); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 89861a8e42..4a93b0c95b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5243,6 +5243,7 @@ phutil_register_library_map(array( 'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php', 'PhortuneAccountEmailViewController' => 'applications/phortune/controller/account/PhortuneAccountEmailViewController.php', 'PhortuneAccountHasMemberEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMemberEdgeType.php', + 'PhortuneAccountHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php', 'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php', 'PhortuneAccountManagersController' => 'applications/phortune/controller/account/PhortuneAccountManagersController.php', 'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php', @@ -5302,6 +5303,7 @@ phutil_register_library_map(array( 'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php', 'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php', 'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php', + 'PhortuneMerchantHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasAccountEdgeType.php', 'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php', 'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php', 'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php', @@ -11796,6 +11798,7 @@ phutil_register_library_map(array( 'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType', 'PhortuneAccountEmailViewController' => 'PhortuneAccountController', 'PhortuneAccountHasMemberEdgeType' => 'PhabricatorEdgeType', + 'PhortuneAccountHasMerchantEdgeType' => 'PhabricatorEdgeType', 'PhortuneAccountListController' => 'PhortuneController', 'PhortuneAccountManagersController' => 'PhortuneAccountProfileController', 'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType', @@ -11866,6 +11869,7 @@ phutil_register_library_map(array( 'PhortuneMerchantEditController' => 'PhortuneMerchantController', 'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine', 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneMerchantHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType', diff --git a/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php index 476c802597..67fbda78ec 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php @@ -97,6 +97,28 @@ final class PhortuneAccountDetailsController ->setHeaderText(pht('Managers')) ->appendChild($member_list); + $merchant_list = id(new PHUIObjectItemListView()) + ->setSimple(true) + ->setNoDataString(pht('No purchase history.')); + + $merchant_phids = $account->getMerchantPHIDs(); + $handles = $viewer->loadHandles($merchant_phids); + + foreach ($merchant_phids as $merchant_phid) { + $handle = $handles[$merchant_phid]; + + $merchant = id(new PHUIObjectItemView()) + ->setImageURI($handle->getImageURI()) + ->setHref($handle->getURI()) + ->setHeader($handle->getFullName()); + + $merchant_list->addItem($merchant); + } + + $curtain->newPanel() + ->setHeaderText(pht('Merchants')) + ->appendChild($merchant_list); + return $curtain; } diff --git a/src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php b/src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php new file mode 100644 index 0000000000..3da3da95dd --- /dev/null +++ b/src/applications/phortune/edge/PhortuneAccountHasMerchantEdgeType.php @@ -0,0 +1,11 @@ +getAccount(); + $merchant = $object->getMerchant(); + $account->writeMerchantEdge($merchant); + + return $xactions; + } + + } diff --git a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php index 941f704ab8..69b93582b3 100644 --- a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php +++ b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php @@ -21,7 +21,8 @@ final class PhortuneMerchantPHIDType extends PhabricatorPHIDType { array $phids) { return id(new PhortuneMerchantQuery()) - ->withPHIDs($phids); + ->withPHIDs($phids) + ->needProfileImage(true); } public function loadHandles( @@ -32,10 +33,10 @@ final class PhortuneMerchantPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $merchant = $objects[$phid]; - $id = $merchant->getID(); - - $handle->setName($merchant->getName()); - $handle->setURI("/phortune/merchant/{$id}/"); + $handle + ->setName($merchant->getName()) + ->setURI($merchant->getURI()) + ->setImageURI($merchant->getProfileImageURI()); } } diff --git a/src/applications/phortune/query/PhortuneAccountQuery.php b/src/applications/phortune/query/PhortuneAccountQuery.php index ee8291218c..70c12d9722 100644 --- a/src/applications/phortune/query/PhortuneAccountQuery.php +++ b/src/applications/phortune/query/PhortuneAccountQuery.php @@ -53,13 +53,34 @@ final class PhortuneAccountQuery protected function willFilterPage(array $accounts) { $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($accounts, 'getPHID')) - ->withEdgeTypes(array(PhortuneAccountHasMemberEdgeType::EDGECONST)); + ->withEdgeTypes( + array( + PhortuneAccountHasMemberEdgeType::EDGECONST, + PhortuneAccountHasMerchantEdgeType::EDGECONST, + )); + $query->execute(); foreach ($accounts as $account) { - $member_phids = $query->getDestinationPHIDs(array($account->getPHID())); + $member_phids = $query->getDestinationPHIDs( + array( + $account->getPHID(), + ), + array( + PhortuneAccountHasMemberEdgeType::EDGECONST, + )); $member_phids = array_reverse($member_phids); $account->attachMemberPHIDs($member_phids); + + $merchant_phids = $query->getDestinationPHIDs( + array( + $account->getPHID(), + ), + array( + PhortuneAccountHasMerchantEdgeType::EDGECONST, + )); + $merchant_phids = array_reverse($merchant_phids); + $account->attachMerchantPHIDs($merchant_phids); } return $accounts; diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 16ff6f72d0..6b31805671 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -16,11 +16,13 @@ final class PhortuneAccount extends PhortuneDAO protected $billingAddress; private $memberPHIDs = self::ATTACHABLE; + private $merchantPHIDs = self::ATTACHABLE; public static function initializeNewAccount(PhabricatorUser $actor) { return id(new self()) ->setBillingName('') ->setBillingAddress('') + ->attachMerchantPHIDs(array()) ->attachMemberPHIDs(array()); } @@ -115,6 +117,26 @@ final class PhortuneAccount extends PhortuneDAO $this->getID()); } + public function attachMerchantPHIDs(array $merchant_phids) { + $this->merchantPHIDs = $merchant_phids; + return $this; + } + + public function getMerchantPHIDs() { + return $this->assertAttached($this->merchantPHIDs); + } + + public function writeMerchantEdge(PhortuneMerchant $merchant) { + $edge_src = $this->getPHID(); + $edge_type = PhortuneAccountHasMerchantEdgeType::EDGECONST; + $edge_dst = $merchant->getPHID(); + + id(new PhabricatorEdgeEditor()) + ->addEdge($edge_src, $edge_type, $edge_dst) + ->save(); + + return $this; + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php index a996dbf5d2..41fc80c2e7 100644 --- a/src/applications/phortune/storage/PhortuneSubscription.php +++ b/src/applications/phortune/storage/PhortuneSubscription.php @@ -161,6 +161,10 @@ final class PhortuneSubscription extends PhortuneDAO } $this->saveTransaction(); + $account = $this->getAccount(); + $merchant = $this->getMerchant(); + $account->writeMerchantEdge($merchant); + return $result; } From a3213ab20be6127e3b3db73533b0fd3c26826ae1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 15 Aug 2019 16:37:46 -0700 Subject: [PATCH 103/122] In Phortune, use actual merchant authority (not authority grants) to control account visibility Summary: Depends on D20715. Ref T13366. See that task for discussion. Replace the unreliable "grantAuthority()"-based check with an actual "can the viewer edit any merchant this account has a relationship with?" check. This makes these objects easier to use from a policy perspective and makes it so that the `Query` alone can fully enforce permissions properly with no setup, so general infrastructure (like handles and transactions) works properly with Phortune objects. Test Plan: Viewed merchants and accounts as users with no authority, direct authority on the account, and indirect authority via a merchant relationship. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20716 --- ...hortuneMerchantInvoiceCreateController.php | 7 +- .../phortune/query/PhortuneMerchantQuery.php | 69 +++++++++++++++++-- .../phortune/storage/PhortuneAccount.php | 22 ++++-- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php index 300525cdd2..8bd070853c 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantInvoiceCreateController.php @@ -58,9 +58,10 @@ final class PhortuneMerchantInvoiceCreateController } if (!$target_account) { - $accounts = PhortuneAccountQuery::loadAccountsForUser( - $target_user, - PhabricatorContentSource::newFromRequest($request)); + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($target_user->getPHID())) + ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) diff --git a/src/applications/phortune/query/PhortuneMerchantQuery.php b/src/applications/phortune/query/PhortuneMerchantQuery.php index b6cab7dbc2..aef7d8aaf1 100644 --- a/src/applications/phortune/query/PhortuneMerchantQuery.php +++ b/src/applications/phortune/query/PhortuneMerchantQuery.php @@ -86,14 +86,14 @@ final class PhortuneMerchantQuery if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'merchant.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'merchant.phid IN (%Ls)', $this->phids); } @@ -113,7 +113,7 @@ final class PhortuneMerchantQuery if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, - 'LEFT JOIN %T e ON m.phid = e.src AND e.type = %d', + 'LEFT JOIN %T e ON merchant.phid = e.src AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhortuneMerchantHasMemberEdgeType::EDGECONST); } @@ -126,7 +126,68 @@ final class PhortuneMerchantQuery } protected function getPrimaryTableAlias() { - return 'm'; + return 'merchant'; + } + + public static function canViewersEditMerchants( + array $viewer_phids, + array $merchant_phids) { + + // See T13366 for some discussion. This is an unusual caching construct to + // make policy filtering of Accounts easier. + + foreach ($viewer_phids as $key => $viewer_phid) { + if (!$viewer_phid) { + unset($viewer_phids[$key]); + } + } + + if (!$viewer_phids) { + return array(); + } + + $cache_key = 'phortune.merchant.can-edit'; + $cache = PhabricatorCaches::getRequestCache(); + + $cache_data = $cache->getKey($cache_key); + if (!$cache_data) { + $cache_data = array(); + } + + $load_phids = array(); + foreach ($viewer_phids as $viewer_phid) { + if (!isset($cache_data[$viewer_phid])) { + $load_phids[] = $viewer_phid; + } + } + + $did_write = false; + foreach ($load_phids as $load_phid) { + $merchants = id(new self()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($load_phid)) + ->execute(); + foreach ($merchants as $merchant) { + $cache_data[$load_phid][$merchant->getPHID()] = true; + $did_write = true; + } + } + + if ($did_write) { + $cache->setKey($cache_key, $cache_data); + } + + $results = array(); + foreach ($viewer_phids as $viewer_phid) { + foreach ($merchant_phids as $merchant_phid) { + if (!isset($cache_data[$viewer_phid][$merchant_phid])) { + continue; + } + $results[$viewer_phid][$merchant_phid] = true; + } + } + + return $results; } } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 6b31805671..182c80f40f 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -179,13 +179,18 @@ final class PhortuneAccount extends PhortuneDAO return true; } - // If the viewer is acting on behalf of a merchant, they can see - // payment accounts. + // See T13366. If the viewer can edit any merchant that this payment + // account has a relationship with, they can see the payment account. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - foreach ($viewer->getAuthorities() as $authority) { - if ($authority instanceof PhortuneMerchant) { - return true; - } + $viewer_phids = array($viewer->getPHID()); + $merchant_phids = $this->getMerchantPHIDs(); + + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + $viewer_phids, + $merchant_phids); + + if ($any_edit) { + return true; } } @@ -193,7 +198,10 @@ final class PhortuneAccount extends PhortuneDAO } public function describeAutomaticCapability($capability) { - return pht('Members of an account can always view and edit it.'); + return array( + pht('Members of an account can always view and edit it.'), + pht('Merchants an account has established a relationship can view it.'), + ); } From 0cc7e8eeb850e2b01f9370585d221f529fc16428 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 16 Aug 2019 09:23:59 -0700 Subject: [PATCH 104/122] Update Phortune payment account interfaces to handle merchant vs customer views Summary: Depends on D20716. Ref T13366. This implements the new policy behavior cleanly in all top-level Phortune payment account interfaces. Test Plan: As a merchant with an account relationship (not an account member) and an account member, browsed all account interfaces and attempted to perform edits. As a merchant, saw a reduced-strength view. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20717 --- src/__phutil_library_map__.php | 6 +- .../PhabricatorPhortuneApplication.php | 12 +- .../PhortuneAccountAddManagerController.php | 41 +++--- .../PhortuneAccountChargesController.php | 16 ++- .../account/PhortuneAccountController.php | 136 ++++++++++++++---- .../PhortuneAccountDetailsController.php | 11 +- ...hortuneAccountEmailAddressesController.php | 16 ++- .../PhortuneAccountEmailEditController.php | 20 +-- .../PhortuneAccountEmailViewController.php | 14 +- .../PhortuneAccountManagersController.php | 23 +-- .../PhortuneAccountOrdersController.php | 17 ++- .../PhortuneAccountOverviewController.php | 11 +- ...hortuneAccountPaymentMethodsController.php | 16 ++- .../PhortuneAccountProfileController.php | 39 ++--- .../PhortuneAccountSubscriptionController.php | 24 ++-- .../PhortuneChargeListController.php} | 2 +- .../phortune/storage/PhortuneAccount.php | 14 +- .../phortune/storage/PhortuneAccountEmail.php | 3 +- src/view/layout/AphrontSideNavFilterView.php | 5 + 19 files changed, 277 insertions(+), 149 deletions(-) rename src/applications/phortune/controller/{account/PhortuneAccountChargeListController.php => charge/PhortuneChargeListController.php} (97%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4a93b0c95b..dc935eb460 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5222,7 +5222,6 @@ phutil_register_library_map(array( 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', 'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php', 'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php', - 'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php', 'PhortuneAccountChargesController' => 'applications/phortune/controller/account/PhortuneAccountChargesController.php', 'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php', 'PhortuneAccountDetailsController' => 'applications/phortune/controller/account/PhortuneAccountDetailsController.php', @@ -5277,6 +5276,7 @@ phutil_register_library_map(array( 'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php', 'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', + 'PhortuneChargeListController' => 'applications/phortune/controller/charge/PhortuneChargeListController.php', 'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php', 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php', @@ -11769,10 +11769,9 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), - 'PhortuneAccountAddManagerController' => 'PhortuneController', + 'PhortuneAccountAddManagerController' => 'PhortuneAccountController', 'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType', 'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType', - 'PhortuneAccountChargeListController' => 'PhortuneController', 'PhortuneAccountChargesController' => 'PhortuneAccountProfileController', 'PhortuneAccountController' => 'PhortuneController', 'PhortuneAccountDetailsController' => 'PhortuneAccountProfileController', @@ -11839,6 +11838,7 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), + 'PhortuneChargeListController' => 'PhortuneController', 'PhortuneChargePHIDType' => 'PhabricatorPHIDType', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index fad3d19c66..fbabe97f35 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -39,8 +39,6 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'card/' => array( 'new/' => 'PhortunePaymentMethodCreateController', ), - 'order/(?:query/(?P[^/]+)/)?' - => 'PhortuneCartListController', 'subscription/' => array( '(?:query/(?P[^/]+)/)?' => 'PhortuneSubscriptionListController', @@ -51,8 +49,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'order/(?P\d+)/' => 'PhortuneCartListController', ), + 'order/(?:query/(?P[^/]+)/)?' + => 'PhortuneCartListController', 'charge/(?:query/(?P[^/]+)/)?' - => 'PhortuneAccountChargeListController', + => 'PhortuneChargeListController', ), 'card/(?P\d+)/' => array( 'edit/' => 'PhortunePaymentMethodEditController', @@ -82,16 +82,12 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { ), 'addresses/' => array( '' => 'PhortuneAccountEmailAddressesController', + '(?P\d+)/' => 'PhortuneAccountEmailViewController', $this->getEditRoutePattern('edit/') => 'PhortuneAccountEmailEditController', ), ), ), - 'address/' => array( - '(?P\d+)/' => 'PhortuneAccountEmailViewController', - $this->getEditRoutePattern('edit/') - => 'PhortuneAccountEmailEditController', - ), 'product/' => array( '' => 'PhortuneProductListController', 'view/(?P\d+)/' => 'PhortuneProductViewController', diff --git a/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php b/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php index 0c8c71d968..4a2b42ab69 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountAddManagerController.php @@ -1,23 +1,17 @@ getViewer(); - $id = $request->getURIData('accountID'); + $account = $this->getAccount(); - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } + $id = $account->getID(); $v_managers = array(); $e_managers = null; @@ -53,12 +47,24 @@ final class PhortuneAccountAddManagerController extends PhortuneController { } } + $account_phid = $account->getPHID(); + $handles = $viewer->loadHandles(array($account_phid)); + $handle = $handles[$account_phid]; + $form = id(new AphrontFormView()) - ->setUser($viewer) + ->setViewer($viewer) + ->appendInstructions( + pht( + 'Choose one or more users to add as account managers. Managers '. + 'have full control of the account.')) + ->appendControl( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Payment Account')) + ->setValue($handle->renderLink())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) - ->setLabel(pht('Managers')) + ->setLabel(pht('Add Managers')) ->setName('managerPHIDs') ->setValue($v_managers) ->setError($e_managers)); @@ -69,7 +75,6 @@ final class PhortuneAccountAddManagerController extends PhortuneController { ->setWidth(AphrontDialogView::WIDTH_FORM) ->addCancelButton($account_uri) ->addSubmitButton(pht('Add Managers')); - } } diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php index 6fc0209f94..a899059669 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php @@ -3,25 +3,27 @@ final class PhortuneAccountChargesController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Order History')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Order History')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); $charge_history = $this->buildChargeHistorySection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter( array( + $authority, $charge_history, )); diff --git a/src/applications/phortune/controller/account/PhortuneAccountController.php b/src/applications/phortune/controller/account/PhortuneAccountController.php index a7361254e2..ed48c67383 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountController.php @@ -4,14 +4,34 @@ abstract class PhortuneAccountController extends PhortuneController { private $account; + private $merchants; - protected function getAccount() { - return $this->account; + final public function handleRequest(AphrontRequest $request) { + if ($this->shouldRequireAccountEditCapability()) { + $response = $this->loadAccountForEdit(); + } else { + $response = $this->loadAccountForView(); + } + + if ($response) { + return $response; + } + + return $this->handleAccountRequest($request); } - protected function setAccount(PhortuneAccount $account) { - $this->account = $account; - return $this; + abstract protected function shouldRequireAccountEditCapability(); + abstract protected function handleAccountRequest(AphrontRequest $request); + + final protected function getAccount() { + if ($this->account === null) { + throw new Exception( + pht( + 'Unable to "getAccount()" before loading or setting account '. + 'context.')); + } + + return $this->account; } protected function buildApplicationCrumbs() { @@ -25,44 +45,112 @@ abstract class PhortuneAccountController return $crumbs; } - protected function loadAccount() { - // TODO: Currently, you must be able to edit an account to view the detail - // page, because the account must be broadly visible so merchants can - // process orders but merchants should not be able to see all the details - // of an account. Ideally the profile pages should be visible to merchants, - // too, just with less information. - return $this->loadAccountForEdit(); + private function loadAccountForEdit() { + return $this->loadAccountWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); } - protected function loadAccountForEdit() { + private function loadAccountForView() { + return $this->loadAccountWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } + + private function loadAccountWithCapabilities(array $capabilities) { $viewer = $this->getViewer(); $request = $this->getRequest(); $account_id = $request->getURIData('accountID'); if (!$account_id) { - $account_id = $request->getURIData('id'); - } - - if (!$account_id) { - return new Aphront404Response(); + throw new Exception( + pht( + 'Controller ("%s") extends controller "%s", but is reachable '. + 'with no "accountID" in URI.', + get_class($this), + __CLASS__)); } $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) + ->requireCapabilities($capabilities) ->executeOne(); if (!$account) { return new Aphront404Response(); } - $this->account = $account; + $this->setAccount($account); return null; } + private function setAccount(PhortuneAccount $account) { + $this->account = $account; + + $viewer = $this->getViewer(); + if (!$account->isUserAccountMember($viewer)) { + $merchant_phids = $account->getMerchantPHIDs(); + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs($merchant_phids) + ->withMemberPHIDs(array($viewer->getPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $this->merchants = $merchants; + } else { + $this->merchants = array(); + } + + return $this; + } + + final protected function getMerchants() { + if ($this->merchants === null) { + throw new Exception( + pht( + 'Unable to "getMerchants()" before loading or setting account '. + 'context.')); + } + + return $this->merchants; + } + + final protected function newAccountAuthorityView() { + $viewer = $this->getViewer(); + + $merchants = $this->getMerchants(); + if (!$merchants) { + return null; + } + + $merchant_phids = mpull($merchants, 'getPHID'); + $merchant_handles = $viewer->loadHandles($merchant_phids); + $merchant_handles = iterator_to_array($merchant_handles); + + $merchant_list = mpull($merchant_handles, 'renderLink'); + $merchant_list = phutil_implode_html(', ', $merchant_list); + + $merchant_message = pht( + 'You can view this account because you control %d merchant(s) it '. + 'has a relationship with: %s.', + phutil_count($merchants), + $merchant_list); + + return id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->setErrors( + array( + $merchant_message, + )); + } + } diff --git a/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php index 67fbda78ec..e6003c8159 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountDetailsController.php @@ -3,12 +3,11 @@ final class PhortuneAccountDetailsController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return true; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); @@ -26,6 +25,7 @@ final class PhortuneAccountDetailsController $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); $details = $this->newDetailsView($account); $curtain = $this->buildCurtainView($account); @@ -41,6 +41,7 @@ final class PhortuneAccountDetailsController ->setCurtain($curtain) ->setMainColumn( array( + $authority, $details, $timeline, )); diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php index 1abff653f1..a946e02efb 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailAddressesController.php @@ -3,25 +3,27 @@ final class PhortuneAccountEmailAddressesController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return true; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Email Addresses')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Email Addresses')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); $addresses = $this->buildAddressesSection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter( array( + $authority, $addresses, )); diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php index 0257d574dd..117139fd55 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailEditController.php @@ -3,23 +3,17 @@ final class PhortuneAccountEmailEditController extends PhortuneAccountController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountEditCapability() { + return true; + } + + protected function handleAccountRequest(AphrontRequest $request) { + $account = $this->getAccount(); + $engine = id(new PhortuneAccountEmailEditEngine()) ->setController($this); if (!$request->getURIData('id')) { - - if (!$request->getURIData('accountID')) { - return new Aphront404Response(); - } - - $response = $this->loadAccount(); - if ($response) { - return $response; - } - - $account = $this->getAccount(); - $engine->setAccount($account); } diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php index c9cb2570c3..7732518d72 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -3,20 +3,23 @@ final class PhortuneAccountEmailViewController extends PhortuneAccountController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountEditCapability() { + return true; + } + + protected function handleAccountRequest(AphrontRequest $request) { $viewer = $this->getViewer(); + $account = $this->getAccount(); $address = id(new PhortuneAccountEmailQuery()) ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$address) { return new Aphront404Response(); } - $account = $address->getAccount(); - $this->setAccount($account); - $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb(pht('Email Addresses'), $account->getEmailAddressesURI()) ->addTextCrumb($address->getObjectName()) @@ -61,7 +64,8 @@ final class PhortuneAccountEmailViewController $edit_uri = $this->getApplicationURI( urisprintf( - 'address/edit/%d/', + 'account/%d/addresses/edit/%d/', + $account->getID(), $address->getID())); $curtain = $this->newCurtainView($account); diff --git a/src/applications/phortune/controller/account/PhortuneAccountManagersController.php b/src/applications/phortune/controller/account/PhortuneAccountManagersController.php index f79d15a7f3..538fadeac9 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountManagersController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountManagersController.php @@ -3,26 +3,29 @@ final class PhortuneAccountManagersController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Managers')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Managers')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); $members = $this->buildMembersSection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $members, - )); + ->setFooter( + array( + $authority, + $members, + )); $navigation = $this->buildSideNavView('managers'); diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php index 38327a7099..902d2032ff 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php @@ -3,25 +3,28 @@ final class PhortuneAccountOrdersController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Order History')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Order History')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); + $order_history = $this->newRecentOrdersView($account, 100); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter( array( + $authority, $order_history, )); diff --git a/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php index 52b8127ab3..078599faca 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php @@ -3,12 +3,11 @@ final class PhortuneAccountOverviewController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); @@ -26,6 +25,7 @@ final class PhortuneAccountOverviewController $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); $status = $this->buildStatusView($account, $invoices); $invoices = $this->buildInvoicesSection($account, $invoices); $purchase_history = $this->newRecentOrdersView($account, 10); @@ -34,6 +34,7 @@ final class PhortuneAccountOverviewController ->setHeader($header) ->setFooter( array( + $authority, $status, $invoices, $purchase_history, diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php index 519199fc92..e80e1a0302 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php @@ -3,18 +3,19 @@ final class PhortuneAccountPaymentMethodsController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Payment Methods')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Payment Methods')) + ->setBorder(true); + $authority = $this->newAccountAuthorityView(); $header = $this->buildHeaderView(); $methods = $this->buildPaymentMethodsSection($account); @@ -22,6 +23,7 @@ final class PhortuneAccountPaymentMethodsController ->setHeader($header) ->setFooter( array( + $authority, $methods, )); diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 9de7b0a8de..522ee08108 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -17,13 +17,16 @@ abstract class PhortuneAccountProfileController ->setHeader($title) ->setHeaderIcon('fa-user-circle'); - return $header; - } + if ($this->getMerchants()) { + $customer_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setName(pht('Customer Account')) + ->setColor('indigo') + ->setIcon('fa-credit-card'); + $header->addTag($customer_tag); + } - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - $crumbs->setBorder(true); - return $crumbs; + return $header; } protected function buildSideNavView($filter = null) { @@ -31,6 +34,8 @@ abstract class PhortuneAccountProfileController $account = $this->getAccount(); $id = $account->getID(); + $can_edit = !$this->getMerchants(); + $nav = id(new AphrontSideNavFilterView()) ->setBaseURI(new PhutilURI($this->getApplicationURI())); @@ -42,11 +47,12 @@ abstract class PhortuneAccountProfileController $this->getApplicationURI("/{$id}/"), 'fa-user-circle'); - $nav->addFilter( - 'details', - pht('Account Details'), - $this->getApplicationURI("/account/{$id}/details/"), - 'fa-address-card-o'); + $nav->newLink('details') + ->setName(pht('Account Details')) + ->setHref($this->getApplicationURI("/account/{$id}/details/")) + ->setIcon('fa-address-card-o') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit); $nav->addLabel(pht('Payments')); @@ -82,11 +88,12 @@ abstract class PhortuneAccountProfileController $this->getApplicationURI("/account/{$id}/managers/"), 'fa-group'); - $nav->addFilter( - 'addresses', - pht('Email Addresses'), - $this->getApplicationURI("/account/{$id}/addresses/"), - 'fa-envelope-o'); + $nav->newLink('addresses') + ->setname(pht('Email Addresses')) + ->setHref($this->getApplicationURI("/account/{$id}/addresses/")) + ->setIcon('fa-envelope-o') + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit); $nav->selectFilter($filter); diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php index 418507e9c2..3acefffd6c 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php @@ -3,26 +3,30 @@ final class PhortuneAccountSubscriptionController extends PhortuneAccountProfileController { - public function handleRequest(AphrontRequest $request) { - $response = $this->loadAccount(); - if ($response) { - return $response; - } + protected function shouldRequireAccountEditCapability() { + return false; + } + protected function handleAccountRequest(AphrontRequest $request) { $account = $this->getAccount(); $title = $account->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Subscriptions')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Subscriptions')) + ->setBorder(true); $header = $this->buildHeaderView(); + $authority = $this->newAccountAuthorityView(); + $subscriptions = $this->buildSubscriptionsSection($account); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $subscriptions, - )); + ->setFooter( + array( + $authority, + $subscriptions, + )); $navigation = $this->buildSideNavView('subscriptions'); diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php b/src/applications/phortune/controller/charge/PhortuneChargeListController.php similarity index 97% rename from src/applications/phortune/controller/account/PhortuneAccountChargeListController.php rename to src/applications/phortune/controller/charge/PhortuneChargeListController.php index ed3f901675..b8edb92507 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php +++ b/src/applications/phortune/controller/charge/PhortuneChargeListController.php @@ -1,6 +1,6 @@ getPHID(); + if (!$user_phid) { + return null; + } + + $member_map = array_fuse($this->getMemberPHIDs()); + + return isset($member_map[$user_phid]); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -174,8 +185,7 @@ final class PhortuneAccount extends PhortuneDAO } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - $members = array_fuse($this->getMemberPHIDs()); - if (isset($members[$viewer->getPHID()])) { + if ($this->isUserAccountMember($viewer)) { return true; } diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php index 674a86be4b..5f1ede8414 100644 --- a/src/applications/phortune/storage/PhortuneAccountEmail.php +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -73,7 +73,8 @@ final class PhortuneAccountEmail public function getURI() { return urisprintf( - '/phortune/address/%d/', + '/phortune/account/%d/addresses/%d/', + $this->getAccount()->getID(), $this->getID()); } diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 9abcea5a02..8757a935b0 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -111,6 +111,11 @@ final class AphrontSideNavFilterView extends AphrontView { $key, $name, $uri, PHUIListItemView::TYPE_BUTTON); } + public function newLink($key) { + $this->addFilter($key, ''); + return $this->getMenuView()->getItem($key); + } + private function addThing($key, $name, $uri, $type, $icon = null) { $item = id(new PHUIListItemView()) ->setName($name) From c4e0ac4d2783096e90e7bfddddc648844189c3db Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 16 Aug 2019 10:04:57 -0700 Subject: [PATCH 105/122] Update PhortunePaymentMethod for modern policy interfaces Summary: Depends on D20717. Ref T13366. Make PhortunePaymentMethod use an extended policy interface for consistency with modern approaches. Since Accounts have hard-coded policy behavior (and can't have object policies like "Subscribers") this should have no actual impact on program behavior. This leaves one weird piece in the policy dialog UIs, see T13381. Test Plan: Viewed and edited payment methods as a merchant and account member. Merchants can only view, not edit. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20718 --- src/__phutil_library_map__.php | 4 ++ .../PhortunePaymentMethodPolicyCodex.php | 35 ++++++++++++ ...hortuneAccountPaymentMethodsController.php | 1 - .../query/PhortunePaymentMethodQuery.php | 3 ++ .../storage/PhortunePaymentMethod.php | 54 +++++++++++++++---- .../policy/codex/PhabricatorPolicyCodex.php | 10 ---- 6 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index dc935eb460..5ab1daf723 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5329,6 +5329,7 @@ phutil_register_library_map(array( 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php', 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php', 'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php', + 'PhortunePaymentMethodPolicyCodex' => 'applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php', 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php', 'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php', @@ -11893,11 +11894,14 @@ phutil_register_library_map(array( 'PhortunePaymentMethod' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + 'PhabricatorPolicyCodexInterface', ), 'PhortunePaymentMethodCreateController' => 'PhortuneController', 'PhortunePaymentMethodDisableController' => 'PhortuneController', 'PhortunePaymentMethodEditController' => 'PhortuneController', 'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType', + 'PhortunePaymentMethodPolicyCodex' => 'PhabricatorPolicyCodex', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentProvider' => 'Phobject', 'PhortunePaymentProviderConfig' => array( diff --git a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php new file mode 100644 index 0000000000..99b41508e8 --- /dev/null +++ b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php @@ -0,0 +1,35 @@ +getObject(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Account members may view and edit payment methods.')); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Merchants you have a relationship with may view associated '. + 'payment methods.')); + + return $rules; + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php index e80e1a0302..ec10205451 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php @@ -34,7 +34,6 @@ final class PhortuneAccountPaymentMethodsController ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); - } private function buildPaymentMethodsSection(PhortuneAccount $account) { diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php index 42d54805e6..013fa147ec 100644 --- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php +++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php @@ -53,6 +53,7 @@ final class PhortunePaymentMethodQuery $account = idx($accounts, $method->getAccountPHID()); if (!$account) { unset($methods[$key]); + $this->didRejectResult($method); continue; } $method->attachAccount($account); @@ -72,6 +73,7 @@ final class PhortunePaymentMethodQuery $merchant = idx($merchants, $method->getMerchantPHID()); if (!$merchant) { unset($methods[$key]); + $this->didRejectResult($method); continue; } $method->attachMerchant($merchant); @@ -91,6 +93,7 @@ final class PhortunePaymentMethodQuery $provider_config = idx($provider_configs, $method->getProviderPHID()); if (!$provider_config) { unset($methods[$key]); + $this->didRejectResult($method); continue; } $method->attachProviderConfig($provider_config); diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index 8937d6ee84..9aa88c1cf3 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -4,8 +4,12 @@ * A payment method is a credit card; it is associated with an account and * charges can be made against it. */ -final class PhortunePaymentMethod extends PhortuneDAO - implements PhabricatorPolicyInterface { +final class PhortunePaymentMethod + extends PhortuneDAO + implements + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface, + PhabricatorPolicyCodexInterface { const STATUS_ACTIVE = 'payment:active'; const STATUS_DISABLED = 'payment:disabled'; @@ -148,18 +152,50 @@ final class PhortunePaymentMethod extends PhortuneDAO } public function getPolicy($capability) { - return $this->getAccount()->getPolicy($capability); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return $this->getAccount()->hasAutomaticCapability( - $capability, - $viewer); + + // See T13366. If you can edit the merchant associated with this payment + // method, you can view the payment method. + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { + return true; + } + } + + return false; } - public function describeAutomaticCapability($capability) { - return pht( - 'Members of an account can always view and edit its payment methods.'); + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + + // See T13366. For blanket view and edit permissions on all payment + // methods, you must be able to edit the associated account. + return array( + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), + ); + } + + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new PhortunePaymentMethodPolicyCodex(); } } diff --git a/src/applications/policy/codex/PhabricatorPolicyCodex.php b/src/applications/policy/codex/PhabricatorPolicyCodex.php index 48e6d2f557..8dee2a38d1 100644 --- a/src/applications/policy/codex/PhabricatorPolicyCodex.php +++ b/src/applications/policy/codex/PhabricatorPolicyCodex.php @@ -44,16 +44,6 @@ abstract class PhabricatorPolicyCodex return null; } - final public function getPolicySpecialRuleForCapability($capability) { - foreach ($this->getPolicySpecialRuleDescriptions() as $rule) { - if (in_array($capability, $rule->getCapabilities())) { - return $rule; - } - } - - return null; - } - final protected function newRule() { return new PhabricatorPolicyCodexRuleDescription(); } From 201634848eb644a8ad565796ff56d71860667f49 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 16 Aug 2019 10:36:30 -0700 Subject: [PATCH 106/122] Make Phortune payment methods transaction-oriented and always support "Add Payment Method" Summary: Depends on D20718. Ref T13366. Ref T13367. - Phortune payment methods currently do not use transactions; update them. - Give them a proper view page with a transaction log. - Add an "Add Payment Method" button which always works. - Show which subscriptions a payment method is associated with. - Get rid of the "Active" status indicator since we now treat "disabled" as "removed", to align with user expectation/intent. - Swap out of some of the super weird div-form-button UI into the new "big, clickable" UI for choice dialogs among a small number of options on a single dimension. Test Plan: - As a mechant-authority and account-authority, created payment methods from carts, subscriptions, and accounts. Edited and viewed payment methods. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13367, T13366 Differential Revision: https://secure.phabricator.com/D20719 --- resources/celerity/map.php | 4 +- .../20190816.payment.01.xaction.sql | 19 + src/__phutil_library_map__.php | 25 +- .../PhabricatorPhortuneApplication.php | 5 +- .../account/PhortuneAccountController.php | 10 +- ...uneAccountPaymentMethodListController.php} | 45 +- ...tuneAccountPaymentMethodViewController.php | 154 ++++++ .../PhortuneAccountSubscriptionController.php | 3 - .../PhortunePaymentMethodCreateController.php | 303 ------------ .../PhortunePaymentMethodCreateController.php | 462 ++++++++++++++++++ ...PhortunePaymentMethodDisableController.php | 19 +- .../PhortunePaymentMethodEditController.php | 46 +- .../editor/PhortunePaymentMethodEditor.php | 18 + .../PhortunePaymentMethodTransactionQuery.php | 10 + .../query/PhortuneSubscriptionQuery.php | 46 +- .../PhortuneSubscriptionSearchEngine.php | 13 - .../phortune/storage/PhortuneAccount.php | 6 + .../phortune/storage/PhortuneMerchant.php | 4 + .../storage/PhortunePaymentMethod.php | 26 +- .../PhortunePaymentMethodTransaction.php | 18 + .../view/PhortuneSubscriptionTableView.php | 20 +- .../PhortunePaymentMethodNameTransaction.php | 39 ++ ...PhortunePaymentMethodStatusTransaction.php | 22 + .../PhortunePaymentMethodTransactionType.php | 4 + .../css/application/phortune/phortune.css | 9 - 25 files changed, 906 insertions(+), 424 deletions(-) create mode 100644 resources/sql/autopatches/20190816.payment.01.xaction.sql rename src/applications/phortune/controller/account/{PhortuneAccountPaymentMethodsController.php => PhortuneAccountPaymentMethodListController.php} (63%) create mode 100644 src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php delete mode 100644 src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php create mode 100644 src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php rename src/applications/phortune/controller/{payment => paymentmethod}/PhortunePaymentMethodDisableController.php (71%) rename src/applications/phortune/controller/{payment => paymentmethod}/PhortunePaymentMethodEditController.php (62%) create mode 100644 src/applications/phortune/editor/PhortunePaymentMethodEditor.php create mode 100644 src/applications/phortune/query/PhortunePaymentMethodTransactionQuery.php create mode 100644 src/applications/phortune/storage/PhortunePaymentMethodTransaction.php create mode 100644 src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php create mode 100644 src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php create mode 100644 src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4eeb433e96..37794cfb81 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -92,7 +92,7 @@ return array( 'rsrc/css/application/pholio/pholio.css' => '88ef5ef1', 'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8', 'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241', - 'rsrc/css/application/phortune/phortune.css' => '12e8251a', + 'rsrc/css/application/phortune/phortune.css' => '508a1a5e', 'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67', 'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0', 'rsrc/css/application/policy/policy-edit.css' => '8794e2ed', @@ -810,7 +810,7 @@ return array( 'pholio-inline-comments-css' => '722b48c2', 'phortune-credit-card-form' => 'd12d214f', 'phortune-credit-card-form-css' => '3b9868a8', - 'phortune-css' => '12e8251a', + 'phortune-css' => '508a1a5e', 'phortune-invoice-css' => '4436b241', 'phrequent-css' => 'bd79cc67', 'phriction-document-css' => '03380da0', diff --git a/resources/sql/autopatches/20190816.payment.01.xaction.sql b/resources/sql/autopatches/20190816.payment.01.xaction.sql new file mode 100644 index 0000000000..22d7baae7e --- /dev/null +++ b/resources/sql/autopatches/20190816.payment.01.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethodtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5ab1daf723..fbf708ca1d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5249,7 +5249,8 @@ phutil_register_library_map(array( 'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php', 'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', - 'PhortuneAccountPaymentMethodsController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php', + 'PhortuneAccountPaymentMethodListController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php', + 'PhortuneAccountPaymentMethodViewController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php', 'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php', @@ -5325,12 +5326,18 @@ phutil_register_library_map(array( 'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php', 'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php', 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', - 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php', - 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php', - 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php', + 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php', + 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php', + 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php', + 'PhortunePaymentMethodEditor' => 'applications/phortune/editor/PhortunePaymentMethodEditor.php', + 'PhortunePaymentMethodNameTransaction' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php', 'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php', 'PhortunePaymentMethodPolicyCodex' => 'applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php', 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', + 'PhortunePaymentMethodStatusTransaction' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php', + 'PhortunePaymentMethodTransaction' => 'applications/phortune/storage/PhortunePaymentMethodTransaction.php', + 'PhortunePaymentMethodTransactionQuery' => 'applications/phortune/query/PhortunePaymentMethodTransactionQuery.php', + 'PhortunePaymentMethodTransactionType' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php', 'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php', 'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php', 'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php', @@ -11805,7 +11812,8 @@ phutil_register_library_map(array( 'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController', 'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', - 'PhortuneAccountPaymentMethodsController' => 'PhortuneAccountProfileController', + 'PhortuneAccountPaymentMethodListController' => 'PhortuneAccountProfileController', + 'PhortuneAccountPaymentMethodViewController' => 'PhortuneAccountController', 'PhortuneAccountProfileController' => 'PhortuneAccountController', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController', @@ -11896,13 +11904,20 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', 'PhabricatorPolicyCodexInterface', + 'PhabricatorApplicationTransactionInterface', ), 'PhortunePaymentMethodCreateController' => 'PhortuneController', 'PhortunePaymentMethodDisableController' => 'PhortuneController', 'PhortunePaymentMethodEditController' => 'PhortuneController', + 'PhortunePaymentMethodEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortunePaymentMethodNameTransaction' => 'PhortunePaymentMethodTransactionType', 'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType', 'PhortunePaymentMethodPolicyCodex' => 'PhabricatorPolicyCodex', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortunePaymentMethodStatusTransaction' => 'PhortunePaymentMethodTransactionType', + 'PhortunePaymentMethodTransaction' => 'PhabricatorModularTransaction', + 'PhortunePaymentMethodTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortunePaymentMethodTransactionType' => 'PhabricatorModularTransactionType', 'PhortunePaymentProvider' => 'Phobject', 'PhortunePaymentProviderConfig' => array( 'PhortuneDAO', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index fbabe97f35..8ec159d024 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -72,7 +72,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '(?P\d+)/' => array( 'details/' => 'PhortuneAccountDetailsController', - 'methods/' => 'PhortuneAccountPaymentMethodsController', + 'methods/' => array( + '' => 'PhortuneAccountPaymentMethodListController', + '(?P\d+)/' => 'PhortuneAccountPaymentMethodViewController', + ), 'orders/' => 'PhortuneAccountOrdersController', 'charges/' => 'PhortuneAccountChargesController', 'subscriptions/' => 'PhortuneAccountSubscriptionController', diff --git a/src/applications/phortune/controller/account/PhortuneAccountController.php b/src/applications/phortune/controller/account/PhortuneAccountController.php index ed48c67383..8b1a44a81a 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountController.php @@ -23,6 +23,10 @@ abstract class PhortuneAccountController abstract protected function shouldRequireAccountEditCapability(); abstract protected function handleAccountRequest(AphrontRequest $request); + private function hasAccount() { + return (bool)$this->account; + } + final protected function getAccount() { if ($this->account === null) { throw new Exception( @@ -37,8 +41,10 @@ abstract class PhortuneAccountController protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - $account = $this->getAccount(); - if ($account) { + // If we hit a policy exception, we can make it here without finding + // an account. + if ($this->hasAccount()) { + $account = $this->getAccount(); $crumbs->addTextCrumb($account->getName(), $account->getURI()); } diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php similarity index 63% rename from src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php rename to src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php index ec10205451..5fa7cdf4e6 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php @@ -1,6 +1,6 @@ getID(); - // TODO: Allow adding a card here directly $add = id(new PHUIButtonView()) ->setTag('a') - ->setText(pht('New Payment Method')) + ->setText(pht('Add Payment Method')) ->setIcon('fa-plus') - ->setHref($this->getApplicationURI("{$id}/card/new/")); + ->setHref($this->getApplicationURI("{$id}/card/new/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); $header = id(new PHUIHeaderView()) - ->setHeader(pht('Payment Methods')); + ->setHeader(pht('Payment Methods')) + ->addActionLink($add); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) @@ -74,39 +76,14 @@ final class PhortuneAccountPaymentMethodsController foreach ($methods as $method) { $id = $method->getID(); - $item = new PHUIObjectItemView(); - $item->setHeader($method->getFullDisplayName()); - - switch ($method->getStatus()) { - case PhortunePaymentMethod::STATUS_ACTIVE: - $item->setStatusIcon('fa-check green'); - - $disable_uri = $this->getApplicationURI('card/'.$id.'/disable/'); - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-times') - ->setHref($disable_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(true)); - break; - case PhortunePaymentMethod::STATUS_DISABLED: - $item->setStatusIcon('fa-ban lightbluetext'); - $item->setDisabled(true); - break; - } + $item = id(new PHUIObjectItemView()) + ->setObjectName($method->getObjectName()) + ->setHeader($method->getFullDisplayName()) + ->setHref($method->getURI()); $provider = $method->buildPaymentProvider(); $item->addAttribute($provider->getPaymentMethodProviderDescription()); - $edit_uri = $this->getApplicationURI('card/'.$id.'/edit/'); - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-pencil') - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - $list->addItem($item); } diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php new file mode 100644 index 0000000000..00c7e9798e --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php @@ -0,0 +1,154 @@ +getViewer(); + $account = $this->getAccount(); + + $method = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('id'))) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->executeOne(); + if (!$method) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Payment Methods'), $account->getPaymentMethodsURI()) + ->addTextCrumb($method->getObjectName()) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader($method->getFullDisplayName()); + + $details = $this->newDetailsView($method); + + $timeline = $this->buildTransactionTimeline( + $method, + new PhortunePaymentMethodTransactionQuery()); + $timeline->setShouldTerminate(true); + + $autopay = $this->newAutopayView($method); + + $curtain = $this->buildCurtainView($method); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $autopay, + $timeline, + )); + + return $this->newPage() + ->setTitle($method->getObjectName()) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildCurtainView(PhortunePaymentMethod $method) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $method, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'card/%d/edit/', + $method->getID())); + + $remove_uri = $this->getApplicationURI( + urisprintf( + 'card/%d/disable/', + $method->getID())); + + $curtain = $this->newCurtainView($method); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Payment Method')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Remove Payment Method')) + ->setIcon('fa-times') + ->setHref($remove_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + return $curtain; + } + + private function newDetailsView(PhortunePaymentMethod $method) { + $viewer = $this->getViewer(); + + $merchant_phid = $method->getMerchantPHID(); + $handles = $viewer->loadHandles( + array( + $merchant_phid, + )); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + if (strlen($method->getName())) { + $view->addProperty(pht('Name'), $method->getDisplayName()); + } + + $view->addProperty(pht('Summary'), $method->getSummary()); + $view->addProperty(pht('Expires'), $method->getDisplayExpires()); + + $view->addProperty( + pht('Merchant'), + $handles[$merchant_phid]->renderLink()); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Payment Method Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } + + private function newAutopayView(PhortunePaymentMethod $method) { + $viewer = $this->getViewer(); + + $subscriptions = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withPaymentMethodPHIDs(array($method->getPHID())) + ->execute(); + + $table = id(new PhortuneSubscriptionTableView()) + ->setViewer($viewer) + ->setSubscriptions($subscriptions) + ->newTableView(); + + $table->setNoDataString( + pht( + 'This payment method is not the default payment method for '. + 'any subscriptions.')); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Autopay Subscriptions')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php index 3acefffd6c..779721c4f3 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionController.php @@ -47,11 +47,8 @@ final class PhortuneAccountSubscriptionController ->setLimit(25) ->execute(); - $handles = $this->loadViewerHandles(mpull($subscriptions, 'getPHID')); - $table = id(new PhortuneSubscriptionTableView()) ->setUser($viewer) - ->setHandles($handles) ->setSubscriptions($subscriptions); $header = id(new PHUIHeaderView()) diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php deleted file mode 100644 index c068862631..0000000000 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php +++ /dev/null @@ -1,303 +0,0 @@ -getViewer(); - $account_id = $request->getURIData('accountID'); - - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $account_id = $account->getID(); - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getInt('merchantID'))) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $cart_id = $request->getInt('cartID'); - $subscription_id = $request->getInt('subscriptionID'); - if ($cart_id) { - $cancel_uri = $this->getApplicationURI("cart/{$cart_id}/checkout/"); - } else if ($subscription_id) { - $cancel_uri = $this->getApplicationURI( - "{$account_id}/subscription/edit/{$subscription_id}/"); - } else { - $cancel_uri = $this->getApplicationURI($account->getID().'/'); - } - - $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); - if (!$providers) { - throw new Exception( - pht( - 'There are no payment providers enabled that can add payment '. - 'methods.')); - } - - if (count($providers) == 1) { - // If there's only one provider, always choose it. - $provider_id = head_key($providers); - } else { - $provider_id = $request->getInt('providerID'); - if (empty($providers[$provider_id])) { - $choices = array(); - foreach ($providers as $provider) { - $choices[] = $this->renderSelectProvider($provider); - } - - $content = phutil_tag( - 'div', - array( - 'class' => 'phortune-payment-method-list', - ), - $choices); - - return $this->newDialog() - ->setRenderDialogAsDiv(true) - ->setTitle(pht('Add Payment Method')) - ->appendParagraph(pht('Choose a payment method to add:')) - ->appendChild($content) - ->addCancelButton($cancel_uri); - } - } - - $provider = $providers[$provider_id]; - - $errors = array(); - $display_exception = null; - if ($request->isFormPost() && $request->getBool('isProviderForm')) { - $method = id(new PhortunePaymentMethod()) - ->setAccountPHID($account->getPHID()) - ->setAuthorPHID($viewer->getPHID()) - ->setMerchantPHID($merchant->getPHID()) - ->setProviderPHID($provider->getProviderConfig()->getPHID()) - ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); - - // Limit the rate at which you can attempt to add payment methods. This - // is intended as a line of defense against using Phortune to validate a - // large list of stolen credit card numbers. - - PhabricatorSystemActionEngine::willTakeAction( - array($viewer->getPHID()), - new PhortuneAddPaymentMethodAction(), - 1); - - if (!$errors) { - $errors = $this->processClientErrors( - $provider, - $request->getStr('errors')); - } - - if (!$errors) { - $client_token_raw = $request->getStr('token'); - $client_token = null; - try { - $client_token = phutil_json_decode($client_token_raw); - } catch (PhutilJSONParserException $ex) { - $errors[] = pht( - 'There was an error decoding token information submitted by the '. - 'client. Expected a JSON-encoded token dictionary, received: %s.', - nonempty($client_token_raw, pht('nothing'))); - } - - if (!$provider->validateCreatePaymentMethodToken($client_token)) { - $errors[] = pht( - 'There was an error with the payment token submitted by the '. - 'client. Expected a valid dictionary, received: %s.', - $client_token_raw); - } - - if (!$errors) { - try { - $provider->createPaymentMethodFromRequest( - $request, - $method, - $client_token); - } catch (PhortuneDisplayException $exception) { - $display_exception = $exception; - } catch (Exception $ex) { - $errors = array( - pht('There was an error adding this payment method:'), - $ex->getMessage(), - ); - } - } - } - - if (!$errors && !$display_exception) { - $method->save(); - - // If we added this method on a cart flow, return to the cart to - // check out. - if ($cart_id) { - $next_uri = $this->getApplicationURI( - "cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID()); - } else if ($subscription_id) { - $next_uri = new PhutilURI($cancel_uri); - $next_uri->replaceQueryParam('added', true); - } else { - $account_uri = $this->getApplicationURI($account->getID().'/'); - $next_uri = new PhutilURI($account_uri); - $next_uri->setFragment('payment'); - } - - return id(new AphrontRedirectResponse())->setURI($next_uri); - } else { - if ($display_exception) { - $dialog_body = $display_exception->getView(); - } else { - $dialog_body = id(new PHUIInfoView()) - ->setErrors($errors); - } - - return $this->newDialog() - ->setTitle(pht('Error Adding Payment Method')) - ->appendChild($dialog_body) - ->addCancelButton($request->getRequestURI()); - } - } - - $form = $provider->renderCreatePaymentMethodForm($request, $errors); - - $form - ->setUser($viewer) - ->setAction($request->getRequestURI()) - ->setWorkflow(true) - ->addHiddenInput('providerID', $provider_id) - ->addHiddenInput('cartID', $request->getInt('cartID')) - ->addHiddenInput('subscriptionID', $request->getInt('subscriptionID')) - ->addHiddenInput('isProviderForm', true) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Add Payment Method')) - ->addCancelButton($cancel_uri)); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Method')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Add Payment Method')); - $crumbs->setBorder(true); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Add Payment Method')) - ->setHeaderIcon('fa-plus-square'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $box, - )); - - return $this->newPage() - ->setTitle($provider->getPaymentMethodDescription()) - ->setCrumbs($crumbs) - ->appendChild($view); - - } - - private function renderSelectProvider( - PhortunePaymentProvider $provider) { - - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $description = $provider->getPaymentMethodDescription(); - $icon_uri = $provider->getPaymentMethodIcon(); - $details = $provider->getPaymentMethodProviderDescription(); - - $this->requireResource('phortune-css'); - - $icon = id(new PHUIIconView()) - ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) - ->setSpriteIcon($provider->getPaymentMethodIcon()); - - $button = id(new PHUIButtonView()) - ->setSize(PHUIButtonView::BIG) - ->setColor(PHUIButtonView::GREY) - ->setIcon($icon) - ->setText($description) - ->setSubtext($details) - ->setMetadata(array('disableWorkflow' => true)); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->setAction($request->getRequestURI()) - ->addHiddenInput('providerID', $provider->getProviderConfig()->getID()) - ->appendChild($button); - - return $form; - } - - private function processClientErrors( - PhortunePaymentProvider $provider, - $client_errors_raw) { - - $errors = array(); - - $client_errors = null; - try { - $client_errors = phutil_json_decode($client_errors_raw); - } catch (PhutilJSONParserException $ex) { - $errors[] = pht( - 'There was an error decoding error information submitted by the '. - 'client. Expected a JSON-encoded list of error codes, received: %s.', - nonempty($client_errors_raw, pht('nothing'))); - } - - foreach (array_unique($client_errors) as $key => $client_error) { - $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( - $client_error); - } - - foreach (array_unique($client_errors) as $client_error) { - switch ($client_error) { - case PhortuneErrCode::ERR_CC_INVALID_NUMBER: - $message = pht( - 'The card number you entered is not a valid card number. Check '. - 'that you entered it correctly.'); - break; - case PhortuneErrCode::ERR_CC_INVALID_CVC: - $message = pht( - 'The CVC code you entered is not a valid CVC code. Check that '. - 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. - 'numeric code which usually appears on the back of the card.'); - break; - case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: - $message = pht( - 'The card expiration date is not a valid expiration date. Check '. - 'that you entered it correctly. You can not add an expired card '. - 'as a payment method.'); - break; - default: - $message = $provider->getCreatePaymentMethodErrorMessage( - $client_error); - if (!$message) { - $message = pht( - "There was an unexpected error ('%s') processing payment ". - "information.", - $client_error); - - phlog($message); - } - break; - } - - $errors[$client_error] = $message; - } - - return $errors; - } - -} diff --git a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php new file mode 100644 index 0000000000..d32cb70f37 --- /dev/null +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php @@ -0,0 +1,462 @@ +getViewer(); + + $account_id = $request->getURIData('accountID'); + $account = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withIDs(array($account_id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$account) { + return new Aphront404Response(); + } + + $cart_id = $request->getInt('cartID'); + $subscription_id = $request->getInt('subscriptionID'); + $merchant_id = $request->getInt('merchantID'); + + if ($cart_id) { + $cart = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($cart_id)) + ->executeOne(); + if (!$cart) { + return new Aphront404Response(); + } + + $subscription_phid = $cart->getSubscriptionPHID(); + if ($subscription_phid) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withPHIDs(array($subscription_phid)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + } else { + $subscription = null; + } + + $merchant = $cart->getMerchant(); + + $cart_id = $cart->getID(); + $subscription_id = null; + $merchant_id = null; + + $next_uri = $cart->getCheckoutURI(); + } else if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $cart = null; + $merchant = $subscription->getMerchant(); + + $cart_id = null; + $subscription_id = $subscription->getID(); + $merchant_id = null; + + $next_uri = $subscription->getURI(); + } else if ($merchant_id) { + $merchant_phids = $account->getMerchantPHIDs(); + if ($merchant_phids) { + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($merchant_id)) + ->withPHIDs($merchant_phids) + ->executeOne(); + } else { + $merchant = null; + } + + if (!$merchant) { + return new Aphront404Response(); + } + + $cart = null; + $subscription = null; + + $cart_id = null; + $subscription_id = null; + $merchant_id = $merchant->getID(); + + $next_uri = $account->getPaymentMethodsURI(); + } else { + $next_uri = $account->getPaymentMethodsURI(); + + $merchant_phids = $account->getMerchantPHIDs(); + if ($merchant_phids) { + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs($merchant_phids) + ->needProfileImage(true) + ->execute(); + } else { + $merchants = array(); + } + + if (!$merchants) { + return $this->newDialog() + ->setTitle(pht('No Merchants')) + ->appendParagraph( + pht( + 'You have not established a relationship with any merchants '. + 'yet. Create an order or subscription before adding payment '. + 'methods.')) + ->addCancelButton($next_uri); + } + + // If there's more than one merchant, ask the user to pick which one they + // want to pay. If there's only one, just pick it for them. + if (count($merchants) > 1) { + $menu = $this->newMerchantMenu($merchants); + + $form = id(new AphrontFormView()) + ->appendInstructions( + pht( + 'Choose the merchant you want to pay.')); + + return $this->newDialog() + ->setTitle(pht('Choose a Merchant')) + ->appendForm($form) + ->appendChild($menu) + ->addCancelButton($next_uri); + } + + $cart = null; + $subscription = null; + $merchant = head($merchants); + + $cart_id = null; + $subscription_id = null; + $merchant_id = $merchant->getID(); + } + + $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); + if (!$providers) { + throw new Exception( + pht( + 'There are no payment providers enabled that can add payment '. + 'methods.')); + } + + $state_params = array( + 'cartID' => $cart_id, + 'subscriptionID' => $subscription_id, + 'merchantID' => $merchant_id, + ); + $state_params = array_filter($state_params); + + $state_uri = new PhutilURI($request->getRequestURI()); + foreach ($state_params as $key => $value) { + $state_uri->replaceQueryParam($key, $value); + } + + $provider_id = $request->getInt('providerID'); + if (isset($providers[$provider_id])) { + $provider = $providers[$provider_id]; + } else { + // If there's more than one provider, ask the user to pick how they + // want to pay. If there's only one, just pick it. + if (count($providers) > 1) { + $menu = $this->newProviderMenu($providers, $state_uri); + + return $this->newDialog() + ->setTitle(pht('Choose a Payment Method')) + ->appendChild($menu) + ->addCancelButton($next_uri); + } + + $provider = head($providers); + } + + $provider_id = $provider->getProviderConfig()->getID(); + + $state_params['providerID'] = $provider_id; + + $errors = array(); + $display_exception = null; + if ($request->isFormPost() && $request->getBool('isProviderForm')) { + $method = id(new PhortunePaymentMethod()) + ->setAccountPHID($account->getPHID()) + ->setAuthorPHID($viewer->getPHID()) + ->setMerchantPHID($merchant->getPHID()) + ->setProviderPHID($provider->getProviderConfig()->getPHID()) + ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); + + // Limit the rate at which you can attempt to add payment methods. This + // is intended as a line of defense against using Phortune to validate a + // large list of stolen credit card numbers. + + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhortuneAddPaymentMethodAction(), + 1); + + if (!$errors) { + $errors = $this->processClientErrors( + $provider, + $request->getStr('errors')); + } + + if (!$errors) { + $client_token_raw = $request->getStr('token'); + $client_token = null; + try { + $client_token = phutil_json_decode($client_token_raw); + } catch (PhutilJSONParserException $ex) { + $errors[] = pht( + 'There was an error decoding token information submitted by the '. + 'client. Expected a JSON-encoded token dictionary, received: %s.', + nonempty($client_token_raw, pht('nothing'))); + } + + if (!$provider->validateCreatePaymentMethodToken($client_token)) { + $errors[] = pht( + 'There was an error with the payment token submitted by the '. + 'client. Expected a valid dictionary, received: %s.', + $client_token_raw); + } + + if (!$errors) { + try { + $provider->createPaymentMethodFromRequest( + $request, + $method, + $client_token); + } catch (PhortuneDisplayException $exception) { + $display_exception = $exception; + } catch (Exception $ex) { + $errors = array( + pht('There was an error adding this payment method:'), + $ex->getMessage(), + ); + } + } + } + + if (!$errors && !$display_exception) { + $xactions = array(); + + $xactions[] = $method->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE) + ->setNewValue(true); + + $editor = id(new PhortunePaymentMethodEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($method, $xactions); + + $next_uri = new PhutilURI($next_uri); + + // If we added this method on a cart flow, return to the cart to + // checkout with this payment method selected. + if ($cart_id) { + $next_uri->replaceQueryParam('paymentMethodID', $method->getID()); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } else { + if ($display_exception) { + $dialog_body = $display_exception->getView(); + } else { + $dialog_body = id(new PHUIInfoView()) + ->setErrors($errors); + } + + return $this->newDialog() + ->setTitle(pht('Error Adding Payment Method')) + ->appendChild($dialog_body) + ->addCancelButton($request->getRequestURI()); + } + } + + $form = $provider->renderCreatePaymentMethodForm($request, $errors); + + $form + ->setViewer($viewer) + ->setAction($request->getPath()) + ->setWorkflow(true) + ->addHiddenInput('isProviderForm', true) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Add Payment Method')) + ->addCancelButton($next_uri)); + + foreach ($state_params as $key => $value) { + $form->addHiddenInput($key, $value); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Method')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($form); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Add Payment Method')) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Add Payment Method')) + ->setHeaderIcon('fa-plus-square'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $box, + )); + + return $this->newPage() + ->setTitle($provider->getPaymentMethodDescription()) + ->setCrumbs($crumbs) + ->appendChild($view); + + } + + private function processClientErrors( + PhortunePaymentProvider $provider, + $client_errors_raw) { + + $errors = array(); + + $client_errors = null; + try { + $client_errors = phutil_json_decode($client_errors_raw); + } catch (PhutilJSONParserException $ex) { + $errors[] = pht( + 'There was an error decoding error information submitted by the '. + 'client. Expected a JSON-encoded list of error codes, received: %s.', + nonempty($client_errors_raw, pht('nothing'))); + } + + foreach (array_unique($client_errors) as $key => $client_error) { + $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( + $client_error); + } + + foreach (array_unique($client_errors) as $client_error) { + switch ($client_error) { + case PhortuneErrCode::ERR_CC_INVALID_NUMBER: + $message = pht( + 'The card number you entered is not a valid card number. Check '. + 'that you entered it correctly.'); + break; + case PhortuneErrCode::ERR_CC_INVALID_CVC: + $message = pht( + 'The CVC code you entered is not a valid CVC code. Check that '. + 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. + 'numeric code which usually appears on the back of the card.'); + break; + case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: + $message = pht( + 'The card expiration date is not a valid expiration date. Check '. + 'that you entered it correctly. You can not add an expired card '. + 'as a payment method.'); + break; + default: + $message = $provider->getCreatePaymentMethodErrorMessage( + $client_error); + if (!$message) { + $message = pht( + "There was an unexpected error ('%s') processing payment ". + "information.", + $client_error); + + phlog($message); + } + break; + } + + $errors[$client_error] = $message; + } + + return $errors; + } + + private function newMerchantMenu(array $merchants) { + assert_instances_of($merchants, 'PhortuneMerchant'); + + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($merchants as $merchant) { + $merchant_uri = id(new PhutilURI($request->getRequestURI())) + ->replaceQueryParam('merchantID', $merchant->getID()); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($merchant->getObjectName()) + ->setHeader($merchant->getName()) + ->setHref($merchant_uri) + ->setClickable(true) + ->setImageURI($merchant->getProfileImageURI()); + + $menu->addItem($item); + } + + return $menu; + } + + private function newProviderMenu(array $providers, PhutilURI $state_uri) { + assert_instances_of($providers, 'PhortunePaymentProvider'); + + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($providers as $provider) { + $provider_id = $provider->getProviderConfig()->getID(); + + $provider_uri = id(clone $state_uri) + ->replaceQueryParam('providerID', $provider_id); + + $description = $provider->getPaymentMethodDescription(); + $icon_uri = $provider->getPaymentMethodIcon(); + $details = $provider->getPaymentMethodProviderDescription(); + + $icon = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) + ->setSpriteIcon($icon_uri); + + $item = id(new PHUIObjectItemView()) + ->setHeader($description) + ->setHref($provider_uri) + ->setClickable(true) + ->addAttribute($details) + ->setImageIcon($icon); + + $menu->addItem($item); + } + + return $menu; + } + +} diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php similarity index 71% rename from src/applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php rename to src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php index f5feec8a29..46d75ffc04 100644 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php @@ -26,14 +26,23 @@ final class PhortunePaymentMethodDisableController $account = $method->getAccount(); $account_id = $account->getID(); - $account_uri = $this->getApplicationURI("/account/billing/{$account_id}/"); + $account_uri = $account->getPaymentMethodsURI(); if ($request->isFormPost()) { + $xactions = array(); - // TODO: ApplicationTransactions!!!! - $method - ->setStatus(PhortunePaymentMethod::STATUS_DISABLED) - ->save(); + $xactions[] = $method->getApplicationTransactionTemplate() + ->setTransactionType( + PhortunePaymentMethodStatusTransaction::TRANSACTIONTYPE) + ->setNewValue(PhortunePaymentMethod::STATUS_DISABLED); + + $editor = id(new PhortunePaymentMethodEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($method, $xactions); return id(new AphrontRedirectResponse())->setURI($account_uri); } diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodEditController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php similarity index 62% rename from src/applications/phortune/controller/payment/PhortunePaymentMethodEditController.php rename to src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php index dc23b81ad0..349f08d319 100644 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodEditController.php +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php @@ -20,25 +20,36 @@ final class PhortunePaymentMethodEditController return new Aphront404Response(); } + $next_uri = $method->getURI(); + $account = $method->getAccount(); - $account_uri = $this->getApplicationURI($account->getID().'/'); + $v_name = $method->getName(); if ($request->isFormPost()) { + $v_name = $request->getStr('name'); - $name = $request->getStr('name'); + $xactions = array(); - // TODO: Use ApplicationTransactions + $xactions[] = $method->getApplicationTransactionTemplate() + ->setTransactionType( + PhortunePaymentMethodNameTransaction::TRANSACTIONTYPE) + ->setNewValue($v_name); - $method->setName($name); - $method->save(); + $editor = id(new PhortunePaymentMethodEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); - return id(new AphrontRedirectResponse())->setURI($account_uri); + $editor->applyTransactions($method, $xactions); + + return id(new AphrontRedirectResponse())->setURI($next_uri); } $provider = $method->buildPaymentProvider(); $form = id(new AphrontFormView()) - ->setUser($viewer) + ->setViewer($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) @@ -54,7 +65,7 @@ final class PhortunePaymentMethodEditController ->setValue($method->getDisplayExpires())) ->appendChild( id(new AphrontFormSubmitControl()) - ->addCancelButton($account_uri) + ->addCancelButton($next_uri) ->setValue(pht('Save Changes'))); $box = id(new PHUIObjectBoxView()) @@ -62,11 +73,12 @@ final class PhortunePaymentMethodEditController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($account->getName(), $account_uri); - $crumbs->addTextCrumb($method->getDisplayName()); - $crumbs->addTextCrumb(pht('Edit')); - $crumbs->setBorder(true); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($account->getName(), $account->getURI()) + ->addTextCrumb(pht('Payment Methods'), $account->getPaymentMethodsURI()) + ->addTextCrumb($method->getObjectName(), $method->getURI()) + ->addTextCrumb(pht('Edit')) + ->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Payment Method')) @@ -74,15 +86,15 @@ final class PhortunePaymentMethodEditController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $box, - )); + ->setFooter( + array( + $box, + )); return $this->newPage() ->setTitle(pht('Edit Payment Method')) ->setCrumbs($crumbs) ->appendChild($view); - } } diff --git a/src/applications/phortune/editor/PhortunePaymentMethodEditor.php b/src/applications/phortune/editor/PhortunePaymentMethodEditor.php new file mode 100644 index 0000000000..4b6c8cedb3 --- /dev/null +++ b/src/applications/phortune/editor/PhortunePaymentMethodEditor.php @@ -0,0 +1,18 @@ +paymentMethodPHIDs = $method_phids; + return $this; + } + public function needTriggers($need_triggers) { $this->needTriggers = $need_triggers; return $this; } + public function newResultObject() { + return new PhortuneSubscription(); + } + protected function loadPage() { - $table = new PhortuneSubscription(); - $conn = $table->establishConnection('r'); - - $rows = queryfx_all( - $conn, - 'SELECT subscription.* FROM %T subscription %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($rows); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $subscriptions) { @@ -67,6 +66,7 @@ final class PhortuneSubscriptionQuery $account = idx($accounts, $subscription->getAccountPHID()); if (!$account) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachAccount($account); @@ -86,6 +86,7 @@ final class PhortuneSubscriptionQuery $merchant = idx($merchants, $subscription->getMerchantPHID()); if (!$merchant) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachMerchant($merchant); @@ -112,6 +113,7 @@ final class PhortuneSubscriptionQuery $implementation = idx($implementations, $ref); if (!$implementation) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachImplementation($implementation); @@ -133,6 +135,7 @@ final class PhortuneSubscriptionQuery $trigger = idx($triggers, $subscription->getTriggerPHID()); if (!$trigger) { unset($subscriptions[$key]); + $this->didRejectResult($subscription); continue; } $subscription->attachTrigger($trigger); @@ -142,10 +145,8 @@ final class PhortuneSubscriptionQuery return $subscriptions; } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); - - $where[] = $this->buildPagingClause($conn); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( @@ -182,7 +183,18 @@ final class PhortuneSubscriptionQuery $this->statuses); } - return $this->formatWhereClause($conn, $where); + if ($this->paymentMethodPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'subscription.defaultPaymentMethodPHID IN (%Ls)', + $this->paymentMethodPHIDs); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'subscription'; } public function getQueryApplicationClass() { diff --git a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php index 62d4f79a25..3f4bfb4e84 100644 --- a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php +++ b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php @@ -125,18 +125,6 @@ final class PhortuneSubscriptionSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $subscriptions, - PhabricatorSavedQuery $query) { - $phids = array(); - foreach ($subscriptions as $subscription) { - $phids[] = $subscription->getPHID(); - $phids[] = $subscription->getMerchantPHID(); - $phids[] = $subscription->getAuthorPHID(); - } - return $phids; - } - protected function renderResultList( array $subscriptions, PhabricatorSavedQuery $query, @@ -147,7 +135,6 @@ final class PhortuneSubscriptionSearchEngine $table = id(new PhortuneSubscriptionTableView()) ->setUser($viewer) - ->setHandles($handles) ->setSubscriptions($subscriptions); $merchant = $this->getMerchant(); diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index bf313f37fc..fcb81ceba3 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -117,6 +117,12 @@ final class PhortuneAccount extends PhortuneDAO $this->getID()); } + public function getPaymentMethodsURI() { + return urisprintf( + '/phortune/account/%d/methods/', + $this->getID()); + } + public function attachMerchantPHIDs(array $merchant_phids) { $this->merchantPHIDs = $merchant_phids; return $this; diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index 4916cfede7..830ff8e1d5 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -70,6 +70,10 @@ final class PhortuneMerchant extends PhortuneDAO return $this->assertAttached($this->profileImageFile); } + public function getObjectName() { + return pht('Merchant %d', $this->getID()); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index 9aa88c1cf3..f04621c020 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -9,7 +9,8 @@ final class PhortunePaymentMethod implements PhabricatorPolicyInterface, PhabricatorExtendedPolicyInterface, - PhabricatorPolicyCodexInterface { + PhabricatorPolicyCodexInterface, + PhabricatorApplicationTransactionInterface { const STATUS_ACTIVE = 'payment:active'; const STATUS_DISABLED = 'payment:disabled'; @@ -140,6 +141,29 @@ final class PhortunePaymentMethod return ($this->getStatus() === self::STATUS_ACTIVE); } + public function getURI() { + return urisprintf( + '/phortune/account/%d/methods/%d/', + $this->getAccount()->getID(), + $this->getID()); + } + + public function getObjectName() { + return pht('Payment Method %d', $this->getID()); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhortunePaymentMethodEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhortunePaymentMethodTransaction(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortunePaymentMethodTransaction.php b/src/applications/phortune/storage/PhortunePaymentMethodTransaction.php new file mode 100644 index 0000000000..16ba306d90 --- /dev/null +++ b/src/applications/phortune/storage/PhortunePaymentMethodTransaction.php @@ -0,0 +1,18 @@ +handles = $handles; - return $this; - } - - public function getHandles() { - return $this->handles; - } - public function setSubscriptions(array $subscriptions) { $this->subscriptions = $subscriptions; return $this; @@ -40,9 +30,15 @@ final class PhortuneSubscriptionTableView extends AphrontView { } public function render() { + return $this->newTableView(); + } + + public function newTableView() { $subscriptions = $this->getSubscriptions(); - $handles = $this->getHandles(); - $viewer = $this->getUser(); + $viewer = $this->getViewer(); + + $phids = mpull($subscriptions, 'getPHID'); + $handles = $viewer->loadHandles($phids); $rows = array(); $rowc = array(); diff --git a/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php new file mode 100644 index 0000000000..4e25877e15 --- /dev/null +++ b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php @@ -0,0 +1,39 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old_value = $this->getOldValue(); + $new_value = $this->getNewValue(); + + if (strlen($old_value) && strlen($new_value)) { + return pht( + '%s renamed this payment method from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if (strlen($new_value)) { + return pht( + '%s set the name of this payment method to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s removed the name of this payment method (was: %s).', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + +} diff --git a/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php new file mode 100644 index 0000000000..f53c90057d --- /dev/null +++ b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php @@ -0,0 +1,22 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + return pht( + '%s changed the status of this payment method.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php new file mode 100644 index 0000000000..97c7c3a887 --- /dev/null +++ b/src/applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php @@ -0,0 +1,4 @@ + Date: Fri, 16 Aug 2019 14:43:42 -0700 Subject: [PATCH 107/122] When a page throws an exception and response construction throws another exception, throw an aggregate exception Summary: Depends on D20719. Currently, if a page throws an exception (like a policy exception) and rendering that exception into a response (like a policy dialog) throws another exception (for example, while constructing breadcrumbs), we only show the orginal exception. This is usually the more useful exception, but sometimes we actually care about the other exception. Instead of guessing which one is more likely to be useful, throw them both as an "AggregateException" and let the high-level handler flatten it for display. Test Plan: {F6749312} Differential Revision: https://secure.phabricator.com/D20720 --- .../AphrontApplicationConfiguration.php | 14 ++++-- .../AphrontUnhandledExceptionResponse.php | 48 +++++++++++++++++-- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index a479209125..c24d59ac90 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -312,11 +312,17 @@ final class AphrontApplicationConfiguration if ($response_exception) { // If we encountered an exception while building a normal response, then // encountered another exception while building a response for the first - // exception, just throw the original exception. It is more likely to be - // useful and point at a root cause than the second exception we ran into - // while telling the user about it. + // exception, throw an aggregate exception that will be unpacked by the + // higher-level handler. This is above our pay grade. if ($original_exception) { - throw $original_exception; + throw new PhutilAggregateException( + pht( + 'Encountered a processing exception, then another exception when '. + 'trying to build a response for the first exception.'), + array( + $response_exception, + $original_exception, + )); } // If we built a response successfully and then ran into an exception diff --git a/src/aphront/response/AphrontUnhandledExceptionResponse.php b/src/aphront/response/AphrontUnhandledExceptionResponse.php index 32d612ca50..2c605cf150 100644 --- a/src/aphront/response/AphrontUnhandledExceptionResponse.php +++ b/src/aphront/response/AphrontUnhandledExceptionResponse.php @@ -61,9 +61,39 @@ final class AphrontUnhandledExceptionResponse return 'unhandled-exception'; } - protected function getResponseBody() { - $ex = $this->exception; + private function getExceptionList() { + return $this->expandException($this->exception); + } + private function expandException($root) { + if ($root instanceof PhutilAggregateException) { + $list = array(); + + $list[] = $root; + + foreach ($root->getExceptions() as $ex) { + foreach ($this->expandException($ex) as $child) { + $list[] = $child; + } + } + + return $list; + } + + return array($root); + } + + protected function getResponseBody() { + $body = array(); + + foreach ($this->getExceptionList() as $ex) { + $body[] = $this->newHTMLMessage($ex); + } + + return $body; + } + + private function newHTMLMessage($ex) { if ($ex instanceof AphrontMalformedRequestException) { $title = $ex->getTitle(); } else { @@ -122,12 +152,20 @@ final class AphrontUnhandledExceptionResponse } protected function buildPlainTextResponseString() { - $ex = $this->exception; + $messages = array(); + foreach ($this->getExceptionList() as $exception) { + $messages[] = $this->newPlainTextMessage($exception); + } + + return implode("\n\n", $messages); + } + + private function newPlainTextMessage($exception) { return pht( '%s: %s', - get_class($ex), - $ex->getMessage()); + get_class($exception), + $exception->getMessage()); } } From a542024b6334a027b16afaf79c0b2263546b09bc Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 16 Aug 2019 16:03:23 -0700 Subject: [PATCH 108/122] Update Phortune subscriptions for modern infrastructure Summary: Depends on D20720. Ref T13366. - Use modern policies and policy interfaces. - Use new merchant authority cache. - Add (some) transactions. - Move MFA from pre-upgrade-gate to post-one-shot-check. - Simplify the autopay workflow. - Use the "reloading arrows" icon for subscriptions more consistently. Test Plan: As a merchant-authority and account-authority, viewed, edited, and changed autopay for subscriptions. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20721 --- .../20190816.subscription.01.xaction.sql | 19 + src/__phutil_library_map__.php | 25 +- .../PhabricatorPhortuneApplication.php | 16 +- .../PhortunePaymentMethodPolicyCodex.php | 1 + .../codex/PhortuneSubscriptionPolicyCodex.php | 36 ++ ...hortuneAccountPaymentMethodController.php} | 2 +- ...neAccountSubscriptionAutopayController.php | 137 +++++++ ...rtuneAccountSubscriptionViewController.php | 338 ++++++++++++++++++ ...PhortunePaymentMethodDisableController.php | 41 ++- .../PhortuneSubscriptionViewController.php | 224 ------------ .../editor/PhortuneSubscriptionEditor.php | 18 + .../phid/PhortunePaymentMethodPHIDType.php | 6 +- .../phid/PhortuneSubscriptionPHIDType.php | 8 +- .../PhortuneSubscriptionTransactionQuery.php | 10 + .../storage/PhortunePaymentMethod.php | 1 - .../phortune/storage/PhortuneSubscription.php | 78 ++-- .../PhortuneSubscriptionTransaction.php | 18 + ...PhortuneSubscriptionAutopayTransaction.php | 41 +++ .../PhortuneSubscriptionTransactionType.php | 4 + 19 files changed, 746 insertions(+), 277 deletions(-) create mode 100644 resources/sql/autopatches/20190816.subscription.01.xaction.sql create mode 100644 src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php rename src/applications/phortune/controller/account/{PhortuneAccountPaymentMethodListController.php => PhortuneAccountPaymentMethodController.php} (97%) create mode 100644 src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php delete mode 100644 src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php create mode 100644 src/applications/phortune/editor/PhortuneSubscriptionEditor.php create mode 100644 src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php create mode 100644 src/applications/phortune/storage/PhortuneSubscriptionTransaction.php create mode 100644 src/applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php create mode 100644 src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php diff --git a/resources/sql/autopatches/20190816.subscription.01.xaction.sql b/resources/sql/autopatches/20190816.subscription.01.xaction.sql new file mode 100644 index 0000000000..8866ce3a57 --- /dev/null +++ b/resources/sql/autopatches/20190816.subscription.01.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_subscriptiontransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fbf708ca1d..e1afebccf4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5249,11 +5249,13 @@ phutil_register_library_map(array( 'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php', 'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', - 'PhortuneAccountPaymentMethodListController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php', + 'PhortuneAccountPaymentMethodController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php', 'PhortuneAccountPaymentMethodViewController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php', 'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', + 'PhortuneAccountSubscriptionAutopayController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php', 'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php', + 'PhortuneAccountSubscriptionViewController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php', @@ -5361,16 +5363,21 @@ phutil_register_library_map(array( 'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php', 'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php', 'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php', + 'PhortuneSubscriptionAutopayTransaction' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php', 'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php', 'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php', + 'PhortuneSubscriptionEditor' => 'applications/phortune/editor/PhortuneSubscriptionEditor.php', 'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php', 'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php', 'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php', + 'PhortuneSubscriptionPolicyCodex' => 'applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php', 'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php', 'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php', 'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php', 'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php', - 'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php', + 'PhortuneSubscriptionTransaction' => 'applications/phortune/storage/PhortuneSubscriptionTransaction.php', + 'PhortuneSubscriptionTransactionQuery' => 'applications/phortune/query/PhortuneSubscriptionTransactionQuery.php', + 'PhortuneSubscriptionTransactionType' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php', 'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php', 'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php', 'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php', @@ -11812,11 +11819,13 @@ phutil_register_library_map(array( 'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController', 'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', - 'PhortuneAccountPaymentMethodListController' => 'PhortuneAccountProfileController', + 'PhortuneAccountPaymentMethodController' => 'PhortuneAccountProfileController', 'PhortuneAccountPaymentMethodViewController' => 'PhortuneAccountController', 'PhortuneAccountProfileController' => 'PhortuneAccountController', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneAccountSubscriptionAutopayController' => 'PhortuneAccountController', 'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController', + 'PhortuneAccountSubscriptionViewController' => 'PhortuneAccountController', 'PhortuneAccountTransaction' => 'PhabricatorModularTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType', @@ -11953,17 +11962,25 @@ phutil_register_library_map(array( 'PhortuneSubscription' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + 'PhabricatorPolicyCodexInterface', + 'PhabricatorApplicationTransactionInterface', ), + 'PhortuneSubscriptionAutopayTransaction' => 'PhortuneSubscriptionTransactionType', 'PhortuneSubscriptionCart' => 'PhortuneCartImplementation', 'PhortuneSubscriptionEditController' => 'PhortuneController', + 'PhortuneSubscriptionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneSubscriptionImplementation' => 'Phobject', 'PhortuneSubscriptionListController' => 'PhortuneController', 'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType', + 'PhortuneSubscriptionPolicyCodex' => 'PhabricatorPolicyCodex', 'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation', 'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhortuneSubscriptionTableView' => 'AphrontView', - 'PhortuneSubscriptionViewController' => 'PhortuneController', + 'PhortuneSubscriptionTransaction' => 'PhabricatorModularTransaction', + 'PhortuneSubscriptionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortuneSubscriptionTransactionType' => 'PhabricatorModularTransactionType', 'PhortuneSubscriptionWorker' => 'PhabricatorWorker', 'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider', 'PhragmentBrowseController' => 'PhragmentController', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 8ec159d024..298c43b8c4 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -43,9 +43,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'PhortuneSubscriptionListController', 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', - 'edit/(?P\d+)/' - => 'PhortuneSubscriptionEditController', + => 'PhortuneAccountSubscriptionViewController', 'order/(?P\d+)/' => 'PhortuneCartListController', ), @@ -73,12 +71,18 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '(?P\d+)/' => array( 'details/' => 'PhortuneAccountDetailsController', 'methods/' => array( - '' => 'PhortuneAccountPaymentMethodListController', + '' => 'PhortuneAccountPaymentMethodController', '(?P\d+)/' => 'PhortuneAccountPaymentMethodViewController', ), 'orders/' => 'PhortuneAccountOrdersController', 'charges/' => 'PhortuneAccountChargesController', - 'subscriptions/' => 'PhortuneAccountSubscriptionController', + 'subscriptions/' => array( + '' => 'PhortuneAccountSubscriptionController', + '(?P\d+)/' => array( + 'autopay/(?P\d+)/' + => 'PhortuneAccountSubscriptionAutopayController', + ), + ), 'managers/' => array( '' => 'PhortuneAccountManagersController', 'add/' => 'PhortuneAccountAddManagerController', @@ -124,7 +128,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'PhortuneSubscriptionListController', 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', + => 'PhortuneAccountSubscriptionViewController', 'order/(?P\d+)/' => 'PhortuneCartListController', ), diff --git a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php index 99b41508e8..389580147a 100644 --- a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php +++ b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php @@ -12,6 +12,7 @@ final class PhortunePaymentMethodPolicyCodex ->setCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, )) ->setIsActive(true) ->setDescription( diff --git a/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php b/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php new file mode 100644 index 0000000000..484e215663 --- /dev/null +++ b/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php @@ -0,0 +1,36 @@ +getObject(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Account members may view and edit subscriptions.')); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->setIsActive(true) + ->setDescription( + pht( + 'Merchants you have a relationship with may view associated '. + 'subscriptions.')); + + return $rules; + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php similarity index 97% rename from src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php rename to src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php index 5fa7cdf4e6..8b616701f2 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php @@ -1,6 +1,6 @@ getViewer(); + $account = $this->getAccount(); + + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('subscriptionID'))) + ->withAccountPHIDs(array($account->getPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $method = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('methodID'))) + ->withAccountPHIDs(array($subscription->getAccountPHID())) + ->withMerchantPHIDs(array($subscription->getMerchantPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->executeOne(); + if (!$method) { + return new Aphront404Response(); + } + + $next_uri = $subscription->getURI(); + + $autopay_phid = $subscription->getDefaultPaymentMethodPHID(); + $is_stop = ($autopay_phid === $method->getPHID()); + + if ($request->isFormOrHisecPost()) { + if ($is_stop) { + $new_phid = null; + } else { + $new_phid = $method->getPHID(); + } + + $xactions = array(); + + $xactions[] = $subscription->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneSubscriptionAutopayTransaction::TRANSACTIONTYPE) + ->setNewValue($new_phid); + + $editor = $subscription->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setCancelURI($next_uri); + + $editor->applyTransactions($subscription, $xactions); + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + + $method_phid = $method->getPHID(); + $subscription_phid = $subscription->getPHID(); + + $handles = $viewer->loadHandles( + array( + $method_phid, + $subscription_phid, + )); + + $method_handle = $handles[$method_phid]; + $subscription_handle = $handles[$subscription_phid]; + + $method_display = $method_handle->renderLink(); + $method_display = phutil_tag( + 'strong', + array(), + $method_display); + + $subscription_display = $subscription_handle->renderLink(); + $subscription_display = phutil_tag( + 'strong', + array(), + $subscription_display); + + $body = array(); + if ($is_stop) { + $title = pht('Stop Autopay'); + + $body[] = pht( + 'Remove %s as the automatic payment method for subscription %s?', + $method_display, + $subscription_display); + + $body[] = pht( + 'This payment method will no longer be charged automatically.'); + + $submit = pht('Stop Autopay'); + } else { + $title = pht('Start Autopay'); + + $body[] = pht( + 'Set %s as the automatic payment method for subscription %s?', + $method_display, + $subscription_display); + + $body[] = pht( + 'This payment method will be used to automatically pay future '. + 'charges.'); + + $submit = pht('Start Autopay'); + } + + $dialog = $this->newDialog() + ->setTitle($title) + ->addCancelButton($next_uri) + ->addSubmitButton($submit); + + foreach ($body as $graph) { + $dialog->appendParagraph($graph); + } + + return $dialog; + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php new file mode 100644 index 0000000000..ca8c64816e --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php @@ -0,0 +1,338 @@ +getViewer(); + + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->needTriggers(true) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $subscription, + PhabricatorPolicyCapability::CAN_EDIT); + + $merchant = $subscription->getMerchant(); + $account = $subscription->getAccount(); + + $account_id = $account->getID(); + $subscription_id = $subscription->getID(); + + $title = $subscription->getSubscriptionFullName(); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-retweet'); + + $edit_uri = $subscription->getEditURI(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($subscription->getSubscriptionCrumbName()) + ->setBorder(true); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $next_invoice = $subscription->getTrigger()->getNextEventPrediction(); + $properties->addProperty( + pht('Next Invoice'), + phabricator_datetime($next_invoice, $viewer)); + + $autopay = $this->newAutopayView($subscription); + + $details = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Subscription Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($properties); + + $due_box = $this->buildDueInvoices($subscription); + $invoice_box = $this->buildPastInvoices($subscription); + + $timeline = $this->buildTransactionTimeline( + $subscription, + new PhortuneSubscriptionTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $details, + $autopay, + $due_box, + $invoice_box, + $timeline, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildDueInvoices(PhortuneSubscription $subscription) { + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withSubscriptionPHIDs(array($subscription->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $phids = array(); + foreach ($invoices as $invoice) { + $phids[] = $invoice->getPHID(); + $phids[] = $invoice->getMerchantPHID(); + foreach ($invoice->getPurchases() as $purchase) { + $phids[] = $purchase->getPHID(); + } + } + $handles = $this->loadViewerHandles($phids); + + $invoice_table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($invoices) + ->setIsInvoices(true) + ->setHandles($handles); + + $invoice_header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices Due')); + + return id(new PHUIObjectBoxView()) + ->setHeader($invoice_header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($invoice_table); + } + + private function buildPastInvoices(PhortuneSubscription $subscription) { + $viewer = $this->getViewer(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withSubscriptionPHIDs(array($subscription->getPHID())) + ->needPurchases(true) + ->withStatuses( + array( + PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, + PhortuneCart::STATUS_PURCHASED, + )) + ->setLimit(50) + ->execute(); + + $phids = array(); + foreach ($invoices as $invoice) { + $phids[] = $invoice->getPHID(); + foreach ($invoice->getPurchases() as $purchase) { + $phids[] = $purchase->getPHID(); + } + } + $handles = $this->loadViewerHandles($phids); + + $invoice_table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($invoices) + ->setHandles($handles); + + $account = $subscription->getAccount(); + $merchant = $subscription->getMerchant(); + + $account_id = $account->getID(); + $merchant_id = $merchant->getID(); + $subscription_id = $subscription->getID(); + + $invoices_uri = $this->getApplicationURI( + "{$account_id}/subscription/order/{$subscription_id}/"); + + $invoice_header = id(new PHUIHeaderView()) + ->setHeader(pht('Past Invoices')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($invoices_uri) + ->setText(pht('View All Invoices'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($invoice_header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($invoice_table); + } + + private function newAutopayView(PhortuneSubscription $subscription) { + $viewer = $this->getViewer(); + $account = $subscription->getAccount(); + + $add_method_uri = urisprintf( + '/phortune/account/%d/card/new/?subscriptionID=%s', + $account->getID(), + $subscription->getID()); + $add_method_uri = $this->getApplicationURI($add_method_uri); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $subscription, + PhabricatorPolicyCapability::CAN_EDIT); + + $methods = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($subscription->getAccountPHID())) + ->withMerchantPHIDs(array($subscription->getMerchantPHID())) + ->withStatuses( + array( + PhortunePaymentMethod::STATUS_ACTIVE, + )) + ->execute(); + $methods = mpull($methods, null, 'getPHID'); + + $autopay_phid = $subscription->getDefaultPaymentMethodPHID(); + $autopay_method = idx($methods, $autopay_phid); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Autopay')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-plus') + ->setHref($add_method_uri) + ->setText(pht('Add Payment Method')) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + $methods = array_select_keys($methods, array($autopay_phid)) + $methods; + + $rows = array(); + $rowc = array(); + foreach ($methods as $method) { + $is_autopay = ($autopay_method === $method); + + $remove_uri = urisprintf( + '/card/%d/disable/?subscriptionID=%d', + $method->getID(), + $subscription->getID()); + $remove_uri = $this->getApplicationURI($remove_uri); + + $autopay_uri = urisprintf( + '/account/%d/subscriptions/%d/autopay/%d/', + $account->getID(), + $subscription->getID(), + $method->getID()); + $autopay_uri = $this->getApplicationURI($autopay_uri); + + $remove_button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor('grey') + ->setIcon('fa-times') + ->setText(pht('Delete')) + ->setHref($remove_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + + if ($is_autopay) { + $autopay_button = id(new PHUIButtonView()) + ->setColor('red') + ->setIcon('fa-times') + ->setText(pht('Stop Autopay')); + } else { + if ($autopay_method) { + $make_color = 'grey'; + } else { + $make_color = 'green'; + } + + $autopay_button = id(new PHUIButtonView()) + ->setColor($make_color) + ->setIcon('fa-retweet') + ->setText(pht('Start Autopay')); + } + + $autopay_button + ->setTag('a') + ->setHref($autopay_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + + $rows[] = array( + $method->getID(), + phutil_tag( + 'a', + array( + 'href' => $method->getURI(), + ), + $method->getFullDisplayName()), + $method->getDisplayExpires(), + $autopay_button, + $remove_button, + ); + + if ($is_autopay) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + } + + $method_table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('ID'), + pht('Payment Method'), + pht('Expires'), + null, + null, + )) + ->setRowClasses($rowc) + ->setColumnClasses( + array( + null, + 'pri wide', + null, + 'right', + null, + )); + + if (!$autopay_method) { + $method_table->setNotice( + array( + id(new PHUIIconView())->setIcon('fa-warning yellow'), + ' ', + pht('Autopay is not currently configured for this subscription.'), + )); + } else { + $method_table->setNotice( + array( + id(new PHUIIconView())->setIcon('fa-check green'), + ' ', + pht( + 'Autopay is configured using %s.', + phutil_tag( + 'a', + array( + 'href' => $autopay_method->getURI(), + ), + $autopay_method->getFullDisplayName())), + )); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($method_table); + } + +} diff --git a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php index 46d75ffc04..146ee64a48 100644 --- a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php +++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php @@ -24,6 +24,21 @@ final class PhortunePaymentMethodDisableController return new Aphront400Response(); } + $subscription_id = $request->getInt('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->withAccountPHIDs(array($method->getAccountPHID())) + ->withMerchantPHIDs(array($method->getMerchantPHID())) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + } else { + $subscription = null; + } + $account = $method->getAccount(); $account_id = $account->getID(); $account_uri = $account->getPaymentMethodsURI(); @@ -44,18 +59,32 @@ final class PhortunePaymentMethodDisableController $editor->applyTransactions($method, $xactions); - return id(new AphrontRedirectResponse())->setURI($account_uri); + if ($subscription) { + $next_uri = $subscription->getURI(); + } else { + $next_uri = $account_uri; + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); } + $method_phid = $method->getPHID(); + $handles = $viewer->loadHandles( + array( + $method_phid, + )); + + $method_handle = $handles[$method_phid]; + $method_display = $method_handle->renderLink(); + $method_display = phutil_tag('strong', array(), $method_display); + return $this->newDialog() ->setTitle(pht('Remove Payment Method')) + ->addHiddenInput('subscriptionID', $subscription_id) ->appendParagraph( pht( - 'Remove the payment method "%s" from your account?', - phutil_tag( - 'strong', - array(), - $method->getFullDisplayName()))) + 'Remove the payment method %s from your account?', + $method_display)) ->appendParagraph( pht( 'You will no longer be able to make payments using this payment '. diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php deleted file mode 100644 index 2e78d37d5c..0000000000 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php +++ /dev/null @@ -1,224 +0,0 @@ -getViewer(); - - $authority = $this->loadMerchantAuthority(); - - $subscription_query = id(new PhortuneSubscriptionQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) - ->needTriggers(true); - - if ($authority) { - $subscription_query->withMerchantPHIDs(array($authority->getPHID())); - } - - $subscription = $subscription_query->executeOne(); - if (!$subscription) { - return new Aphront404Response(); - } - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $subscription, - PhabricatorPolicyCapability::CAN_EDIT); - - $merchant = $subscription->getMerchant(); - $account = $subscription->getAccount(); - - $account_id = $account->getID(); - $subscription_id = $subscription->getID(); - - $title = $subscription->getSubscriptionFullName(); - - $header = id(new PHUIHeaderView()) - ->setHeader($title) - ->setHeaderIcon('fa-calendar-o'); - - $curtain = $this->newCurtainView($subscription); - $edit_uri = $subscription->getEditURI(); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-credit-card') - ->setName(pht('Manage Autopay')) - ->setHref($edit_uri) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $crumbs = $this->buildApplicationCrumbs(); - if ($authority) { - $this->addMerchantCrumb($crumbs, $merchant); - } else { - $this->addAccountCrumb($crumbs, $account); - } - $crumbs->addTextCrumb($subscription->getSubscriptionCrumbName()); - $crumbs->setBorder(true); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer); - - $next_invoice = $subscription->getTrigger()->getNextEventPrediction(); - $properties->addProperty( - pht('Next Invoice'), - phabricator_datetime($next_invoice, $viewer)); - - $default_method = $subscription->getDefaultPaymentMethodPHID(); - if ($default_method) { - $method = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withPHIDs(array($default_method)) - ->withStatuses( - array( - PhortunePaymentMethod::STATUS_ACTIVE, - )) - ->executeOne(); - if ($method) { - $handles = $this->loadViewerHandles(array($default_method)); - $autopay_method = $handles[$default_method]->renderLink(); - } else { - $autopay_method = phutil_tag( - 'em', - array(), - pht('')); - } - } else { - $autopay_method = phutil_tag( - 'em', - array(), - pht('No Autopay Method Configured')); - } - - $properties->addProperty( - pht('Autopay With'), - $autopay_method); - - $details = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Details')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addPropertyList($properties); - - $due_box = $this->buildDueInvoices($subscription, $authority); - $invoice_box = $this->buildPastInvoices($subscription, $authority); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $details, - $due_box, - $invoice_box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - } - - private function buildDueInvoices( - PhortuneSubscription $subscription, - $authority) { - $viewer = $this->getViewer(); - - $invoices = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withSubscriptionPHIDs(array($subscription->getPHID())) - ->needPurchases(true) - ->withInvoices(true) - ->execute(); - - $phids = array(); - foreach ($invoices as $invoice) { - $phids[] = $invoice->getPHID(); - $phids[] = $invoice->getMerchantPHID(); - foreach ($invoice->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $invoice_table = id(new PhortuneOrderTableView()) - ->setUser($viewer) - ->setCarts($invoices) - ->setIsInvoices(true) - ->setIsMerchantView((bool)$authority) - ->setHandles($handles); - - $invoice_header = id(new PHUIHeaderView()) - ->setHeader(pht('Invoices Due')); - - return id(new PHUIObjectBoxView()) - ->setHeader($invoice_header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($invoice_table); - } - - private function buildPastInvoices( - PhortuneSubscription $subscription, - $authority) { - $viewer = $this->getViewer(); - - $invoices = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withSubscriptionPHIDs(array($subscription->getPHID())) - ->needPurchases(true) - ->withStatuses( - array( - PhortuneCart::STATUS_PURCHASING, - PhortuneCart::STATUS_CHARGED, - PhortuneCart::STATUS_HOLD, - PhortuneCart::STATUS_REVIEW, - PhortuneCart::STATUS_PURCHASED, - )) - ->setLimit(50) - ->execute(); - - $phids = array(); - foreach ($invoices as $invoice) { - $phids[] = $invoice->getPHID(); - foreach ($invoice->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - - $invoice_table = id(new PhortuneOrderTableView()) - ->setUser($viewer) - ->setCarts($invoices) - ->setHandles($handles); - - $account = $subscription->getAccount(); - $merchant = $subscription->getMerchant(); - - $account_id = $account->getID(); - $merchant_id = $merchant->getID(); - $subscription_id = $subscription->getID(); - - if ($authority) { - $invoices_uri = $this->getApplicationURI( - "merchant/{$merchant_id}/subscription/order/{$subscription_id}/"); - } else { - $invoices_uri = $this->getApplicationURI( - "{$account_id}/subscription/order/{$subscription_id}/"); - } - - $invoice_header = id(new PHUIHeaderView()) - ->setHeader(pht('Past Invoices')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-list') - ->setHref($invoices_uri) - ->setText(pht('View All Invoices'))); - - return id(new PHUIObjectBoxView()) - ->setHeader($invoice_header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($invoice_table); - } - -} diff --git a/src/applications/phortune/editor/PhortuneSubscriptionEditor.php b/src/applications/phortune/editor/PhortuneSubscriptionEditor.php new file mode 100644 index 0000000000..a2314f5650 --- /dev/null +++ b/src/applications/phortune/editor/PhortuneSubscriptionEditor.php @@ -0,0 +1,18 @@ + $handle) { $method = $objects[$phid]; - $id = $method->getID(); - - $handle->setName($method->getFullDisplayName()); + $handle + ->setName($method->getFullDisplayName()) + ->setURI($method->getURI()); } } diff --git a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php index 6d7275c62b..e07dce12f4 100644 --- a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php +++ b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php @@ -32,11 +32,9 @@ final class PhortuneSubscriptionPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $subscription = $objects[$phid]; - $id = $subscription->getID(); - - $handle->setName($subscription->getSubscriptionName()); - $handle->setURI($subscription->getURI()); - + $handle + ->setName($subscription->getSubscriptionName()) + ->setURI($subscription->getURI()); } } diff --git a/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php new file mode 100644 index 0000000000..db97925b39 --- /dev/null +++ b/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php @@ -0,0 +1,10 @@ +getAccount() - ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { - return true; - } - - // If the viewer controls the merchant this subscription bills to, they can - // view the subscription. - if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - $can_admin = PhabricatorPolicyFilter::hasCapability( - $viewer, - $this->getMerchant(), - PhabricatorPolicyCapability::CAN_EDIT); - if ($can_admin) { + // See T13366. If you can edit the merchant associated with this + // subscription, you can view the subscription. + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { return true; } } @@ -284,12 +289,31 @@ final class PhortuneSubscription extends PhortuneDAO return false; } - public function describeAutomaticCapability($capability) { + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + + // See T13366. For blanket view and edit permissions on all subscriptions, + // you must be able to edit the associated account. return array( - pht('Subscriptions inherit the policies of the associated account.'), - pht( - 'The merchant you are subscribed with can review and manage the '. - 'subscription.'), + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), ); } + + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new PhortuneSubscriptionPolicyCodex(); + } + } diff --git a/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php b/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php new file mode 100644 index 0000000000..78054a5aba --- /dev/null +++ b/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php @@ -0,0 +1,18 @@ +getDefaultPaymentMethodPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setDefaultPaymentMethodPHID($value); + } + + public function getTitle() { + $old_phid = $this->getOldValue(); + $new_phid = $this->getNewValue(); + + if ($old_phid && $new_phid) { + return pht( + '%s changed the automatic payment method for this subscription.', + $this->renderAuthor()); + } else if ($new_phid) { + return pht( + '%s configured an automatic payment method for this subscription.', + $this->renderAuthor()); + } else { + return pht( + '%s stopped automatic payments for this subscription.', + $this->renderAuthor()); + } + } + + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return true; + } + +} diff --git a/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php new file mode 100644 index 0000000000..676b36122b --- /dev/null +++ b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php @@ -0,0 +1,4 @@ + Date: Thu, 22 Aug 2019 14:23:23 -0700 Subject: [PATCH 109/122] Update Charge and Cart policies in Phortune, and make URIs more consistent Summary: Ref T13366. Depends on D20721. Continue applying UI and policy updates to the last two Phortune objects. Charges aren't mutable and Carts are already transactional, so this is less involved than prior changes. Test Plan: Viewed various charge/order interfaces as merchants and account members. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20732 --- src/__phutil_library_map__.php | 8 +- .../PhabricatorPhortuneApplication.php | 36 ++++----- .../PhortuneAccountChargeListController.php | 35 +++++++++ .../PhortuneAccountChargesController.php | 2 +- .../account/PhortuneAccountController.php | 2 +- .../PhortuneAccountOrderListController.php | 58 +++++++++++++++ ...PhortuneAccountPaymentMethodController.php | 2 +- .../PhortuneAccountProfileController.php | 18 ++--- ...rtuneAccountSubscriptionViewController.php | 4 +- .../charge/PhortuneChargeListController.php | 74 ------------------- .../query/PhortuneCartSearchEngine.php | 20 +---- .../query/PhortuneChargeSearchEngine.php | 10 --- .../phortune/storage/PhortuneAccount.php | 36 ++++++++- .../phortune/storage/PhortuneCart.php | 42 +++++------ .../phortune/storage/PhortuneCharge.php | 35 +++++++-- .../phortune/storage/PhortuneSubscription.php | 8 +- 16 files changed, 218 insertions(+), 172 deletions(-) create mode 100644 src/applications/phortune/controller/account/PhortuneAccountChargeListController.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountOrderListController.php delete mode 100644 src/applications/phortune/controller/charge/PhortuneChargeListController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e1afebccf4..4f00d449fa 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5222,6 +5222,7 @@ phutil_register_library_map(array( 'PhortuneAccountAddManagerController' => 'applications/phortune/controller/account/PhortuneAccountAddManagerController.php', 'PhortuneAccountBillingAddressTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingAddressTransaction.php', 'PhortuneAccountBillingNameTransaction' => 'applications/phortune/xaction/PhortuneAccountBillingNameTransaction.php', + 'PhortuneAccountChargeListController' => 'applications/phortune/controller/account/PhortuneAccountChargeListController.php', 'PhortuneAccountChargesController' => 'applications/phortune/controller/account/PhortuneAccountChargesController.php', 'PhortuneAccountController' => 'applications/phortune/controller/account/PhortuneAccountController.php', 'PhortuneAccountDetailsController' => 'applications/phortune/controller/account/PhortuneAccountDetailsController.php', @@ -5246,6 +5247,7 @@ phutil_register_library_map(array( 'PhortuneAccountListController' => 'applications/phortune/controller/account/PhortuneAccountListController.php', 'PhortuneAccountManagersController' => 'applications/phortune/controller/account/PhortuneAccountManagersController.php', 'PhortuneAccountNameTransaction' => 'applications/phortune/xaction/PhortuneAccountNameTransaction.php', + 'PhortuneAccountOrderListController' => 'applications/phortune/controller/account/PhortuneAccountOrderListController.php', 'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php', 'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', @@ -5279,7 +5281,6 @@ phutil_register_library_map(array( 'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php', 'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', - 'PhortuneChargeListController' => 'applications/phortune/controller/charge/PhortuneChargeListController.php', 'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php', 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 'PhortuneChargeSearchEngine' => 'applications/phortune/query/PhortuneChargeSearchEngine.php', @@ -11787,6 +11788,7 @@ phutil_register_library_map(array( 'PhortuneAccountAddManagerController' => 'PhortuneAccountController', 'PhortuneAccountBillingAddressTransaction' => 'PhortuneAccountTransactionType', 'PhortuneAccountBillingNameTransaction' => 'PhortuneAccountTransactionType', + 'PhortuneAccountChargeListController' => 'PhortuneAccountProfileController', 'PhortuneAccountChargesController' => 'PhortuneAccountProfileController', 'PhortuneAccountController' => 'PhortuneController', 'PhortuneAccountDetailsController' => 'PhortuneAccountProfileController', @@ -11816,6 +11818,7 @@ phutil_register_library_map(array( 'PhortuneAccountListController' => 'PhortuneController', 'PhortuneAccountManagersController' => 'PhortuneAccountProfileController', 'PhortuneAccountNameTransaction' => 'PhortuneAccountTransactionType', + 'PhortuneAccountOrderListController' => 'PhortuneAccountProfileController', 'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController', 'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', @@ -11836,6 +11839,7 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', ), 'PhortuneCartAcceptController' => 'PhortuneCartController', 'PhortuneCartCancelController' => 'PhortuneCartController', @@ -11855,8 +11859,8 @@ phutil_register_library_map(array( 'PhortuneCharge' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', ), - 'PhortuneChargeListController' => 'PhortuneController', 'PhortuneChargePHIDType' => 'PhabricatorPHIDType', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneChargeSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 298c43b8c4..20dbc5c88f 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -34,24 +34,6 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { return array( '/phortune/' => array( '' => 'PhortuneLandingController', - '(?P\d+)/' => array( - '' => 'PhortuneAccountOverviewController', - 'card/' => array( - 'new/' => 'PhortunePaymentMethodCreateController', - ), - 'subscription/' => array( - '(?:query/(?P[^/]+)/)?' - => 'PhortuneSubscriptionListController', - 'view/(?P\d+)/' - => 'PhortuneAccountSubscriptionViewController', - 'order/(?P\d+)/' - => 'PhortuneCartListController', - ), - 'order/(?:query/(?P[^/]+)/)?' - => 'PhortuneCartListController', - 'charge/(?:query/(?P[^/]+)/)?' - => 'PhortuneChargeListController', - ), 'card/(?P\d+)/' => array( 'edit/' => 'PhortunePaymentMethodEditController', 'disable/' => 'PhortunePaymentMethodDisableController', @@ -65,22 +47,36 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { ), 'account/' => array( '' => 'PhortuneAccountListController', + $this->getEditRoutePattern('edit/') => 'PhortuneAccountEditController', '(?P\d+)/' => array( + '' => 'PhortuneAccountOverviewController', 'details/' => 'PhortuneAccountDetailsController', 'methods/' => array( '' => 'PhortuneAccountPaymentMethodController', '(?P\d+)/' => 'PhortuneAccountPaymentMethodViewController', + 'new/' => 'PhortunePaymentMethodCreateController', + ), + 'orders/' => array( + '' => 'PhortuneAccountOrdersController', + $this->getQueryRoutePattern('list/') + => 'PhortuneAccountOrderListController', + ), + 'charges/' => array( + '' => 'PhortuneAccountChargesController', + $this->getQueryRoutePattern('list/') + => 'PhortuneAccountChargeListController', ), - 'orders/' => 'PhortuneAccountOrdersController', - 'charges/' => 'PhortuneAccountChargesController', 'subscriptions/' => array( '' => 'PhortuneAccountSubscriptionController', '(?P\d+)/' => array( + '' => 'PhortuneAccountSubscriptionViewController', 'autopay/(?P\d+)/' => 'PhortuneAccountSubscriptionAutopayController', + $this->getQueryRoutePattern('orders/') + => 'PhortuneAccountOrderListController', ), ), 'managers/' => array( diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php b/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php new file mode 100644 index 0000000000..882e37f3de --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountChargeListController.php @@ -0,0 +1,35 @@ +getViewer(); + $account = $this->getAccount(); + + return id(new PhortuneChargeSearchEngine()) + ->setAccount($account) + ->setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + if ($this->hasAccount()) { + $account = $this->getAccount(); + $id = $account->getID(); + + $crumbs->addTextCrumb( + pht('Charges'), + $account->getChargesURI()); + } + + return $crumbs; + } + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php index a899059669..db85698be7 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php @@ -56,7 +56,7 @@ final class PhortuneAccountChargesController $handles = $this->loadViewerHandles($phids); - $charges_uri = $this->getApplicationURI($account->getID().'/charge/'); + $charges_uri = $account->getChargeListURI(); $table = id(new PhortuneChargeTableView()) ->setUser($viewer) diff --git a/src/applications/phortune/controller/account/PhortuneAccountController.php b/src/applications/phortune/controller/account/PhortuneAccountController.php index 8b1a44a81a..d3d60d2789 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountController.php @@ -23,7 +23,7 @@ abstract class PhortuneAccountController abstract protected function shouldRequireAccountEditCapability(); abstract protected function handleAccountRequest(AphrontRequest $request); - private function hasAccount() { + final protected function hasAccount() { return (bool)$this->account; } diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php b/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php new file mode 100644 index 0000000000..fd4a3c389f --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php @@ -0,0 +1,58 @@ +getViewer(); + $account = $this->getAccount(); + + $engine = id(new PhortuneCartSearchEngine()) + ->setController($this) + ->setAccount($account); + + $subscription_id = $request->getURIData('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $engine->setSubscription($subscription); + $this->subscription = $subscription; + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $subscription = $this->subscription; + if ($subscription) { + $crumbs->addTextCrumb( + $subscription->getObjectName(), + $subscription->getURI()); + } else if ($this->hasAccount()) { + $account = $this->getAccount(); + $id = $account->getID(); + + $crumbs->addTextCrumb( + pht('Orders'), + $account->getOrdersURI()); + } + + return $crumbs; + } + + +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php index 8b616701f2..05a2f33d8d 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php @@ -50,7 +50,7 @@ final class PhortuneAccountPaymentMethodController ->setTag('a') ->setText(pht('Add Payment Method')) ->setIcon('fa-plus') - ->setHref($this->getApplicationURI("{$id}/card/new/")) + ->setHref($this->getApplicationURI("account/{$id}/methods/new/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 522ee08108..739316805f 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -3,10 +3,6 @@ abstract class PhortuneAccountProfileController extends PhortuneAccountController { - public function buildApplicationMenu() { - return $this->buildSideNavView()->getMenu(); - } - protected function buildHeaderView() { $viewer = $this->getViewer(); $account = $this->getAccount(); @@ -44,7 +40,7 @@ abstract class PhortuneAccountProfileController $nav->addFilter( 'overview', pht('Overview'), - $this->getApplicationURI("/{$id}/"), + $account->getURI(), 'fa-user-circle'); $nav->newLink('details') @@ -59,25 +55,25 @@ abstract class PhortuneAccountProfileController $nav->addFilter( 'methods', pht('Payment Methods'), - $this->getApplicationURI("/account/{$id}/methods/"), + $account->getPaymentMethodsURI(), 'fa-credit-card'); $nav->addFilter( 'subscriptions', pht('Subscriptions'), - $this->getApplicationURI("/account/{$id}/subscriptions/"), + $account->getSubscriptionsURI(), 'fa-retweet'); $nav->addFilter( 'orders', pht('Order History'), - $this->getApplicationURI("/account/{$id}/orders/"), + $account->getOrdersURI(), 'fa-shopping-bag'); $nav->addFilter( 'charges', pht('Charge History'), - $this->getApplicationURI("/account/{$id}/charges/"), + $account->getChargesURI(), 'fa-calculator'); $nav->addLabel(pht('Personnel')); @@ -90,7 +86,7 @@ abstract class PhortuneAccountProfileController $nav->newLink('addresses') ->setname(pht('Email Addresses')) - ->setHref($this->getApplicationURI("/account/{$id}/addresses/")) + ->setHref($account->getEmailAddressesURI()) ->setIcon('fa-envelope-o') ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit); @@ -130,7 +126,7 @@ abstract class PhortuneAccountProfileController } $handles = $this->loadViewerHandles($phids); - $orders_uri = $this->getApplicationURI($account->getID().'/order/'); + $orders_uri = $account->getOrderListURI(); $table = id(new PhortuneOrderTableView()) ->setUser($viewer) diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php index ca8c64816e..c3e39b94d7 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php @@ -12,7 +12,7 @@ final class PhortuneAccountSubscriptionViewController $subscription = id(new PhortuneSubscriptionQuery()) ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) + ->withIDs(array($request->getURIData('subscriptionID'))) ->needTriggers(true) ->executeOne(); if (!$subscription) { @@ -179,7 +179,7 @@ final class PhortuneAccountSubscriptionViewController $account = $subscription->getAccount(); $add_method_uri = urisprintf( - '/phortune/account/%d/card/new/?subscriptionID=%s', + '/account/%d/methods/new/?subscriptionID=%s', $account->getID(), $subscription->getID()); $add_method_uri = $this->getApplicationURI($add_method_uri); diff --git a/src/applications/phortune/controller/charge/PhortuneChargeListController.php b/src/applications/phortune/controller/charge/PhortuneChargeListController.php deleted file mode 100644 index b8edb92507..0000000000 --- a/src/applications/phortune/controller/charge/PhortuneChargeListController.php +++ /dev/null @@ -1,74 +0,0 @@ -getViewer(); - $querykey = $request->getURIData('queryKey'); - $account_id = $request->getURIData('accountID'); - - $engine = new PhortuneChargeSearchEngine(); - - if ($account_id) { - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $this->account = $account; - $engine->setAccount($account); - } else { - return new Aphront404Response(); - } - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine($engine) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getViewer(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhortuneChargeSearchEngine()) - ->setViewer($viewer) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $account = $this->account; - if ($account) { - $id = $account->getID(); - $crumbs->addTextCrumb( - $account->getName(), - $this->getApplicationURI("{$id}/")); - $crumbs->addTextCrumb( - pht('Charges'), - $this->getApplicationURI("{$id}/charge/")); - } - - return $crumbs; - } - -} diff --git a/src/applications/phortune/query/PhortuneCartSearchEngine.php b/src/applications/phortune/query/PhortuneCartSearchEngine.php index 193764c2cd..da719c17c0 100644 --- a/src/applications/phortune/query/PhortuneCartSearchEngine.php +++ b/src/applications/phortune/query/PhortuneCartSearchEngine.php @@ -62,26 +62,8 @@ final class PhortuneCartSearchEngine $merchant = $this->getMerchant(); $account = $this->getAccount(); if ($merchant) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $merchant, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - throw new Exception( - pht('You can not query orders for a merchant you do not control.')); - } $query->withMerchantPHIDs(array($merchant->getPHID())); } else if ($account) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - throw new Exception( - pht( - 'You can not query orders for an account you are not '. - 'a member of.')); - } $query->withAccountPHIDs(array($account->getPHID())); } else { $accounts = id(new PhortuneAccountQuery()) @@ -125,7 +107,7 @@ final class PhortuneCartSearchEngine if ($merchant) { return '/phortune/merchant/orders/'.$merchant->getID().'/'.$path; } else if ($account) { - return '/phortune/'.$account->getID().'/order/'.$path; + return $account->getOrderListURI($path); } else { return '/phortune/order/'.$path; } diff --git a/src/applications/phortune/query/PhortuneChargeSearchEngine.php b/src/applications/phortune/query/PhortuneChargeSearchEngine.php index 0d6c2cfd59..45316118af 100644 --- a/src/applications/phortune/query/PhortuneChargeSearchEngine.php +++ b/src/applications/phortune/query/PhortuneChargeSearchEngine.php @@ -40,16 +40,6 @@ final class PhortuneChargeSearchEngine $account = $this->getAccount(); if ($account) { - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - throw new Exception( - pht( - 'You can not query charges for an account you are not '. - 'a member of.')); - } $query->withAccountPHIDs(array($account->getPHID())); } else { $accounts = id(new PhortuneAccountQuery()) diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index fcb81ceba3..c823b6a811 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -102,7 +102,9 @@ final class PhortuneAccount extends PhortuneDAO } public function getURI() { - return '/phortune/'.$this->getID().'/'; + return urisprintf( + '/phortune/account/%d/', + $this->getID()); } public function getDetailsURI() { @@ -111,6 +113,25 @@ final class PhortuneAccount extends PhortuneDAO $this->getID()); } + public function getOrdersURI() { + return urisprintf( + '/phortune/account/%d/orders/', + $this->getID()); + } + + public function getOrderListURI($path = '') { + return urisprintf( + '/phortune/account/%d/orders/list/%s', + $this->getID(), + $path); + } + + public function getSubscriptionsURI() { + return urisprintf( + '/phortune/account/%d/subscriptions/', + $this->getID()); + } + public function getEmailAddressesURI() { return urisprintf( '/phortune/account/%d/addresses/', @@ -123,6 +144,19 @@ final class PhortuneAccount extends PhortuneDAO $this->getID()); } + public function getChargesURI() { + return urisprintf( + '/phortune/account/%d/charges/', + $this->getID()); + } + + public function getChargeListURI($path = '') { + return urisprintf( + '/phortune/account/%d/charges/list/%s', + $this->getID(), + $path); + } + public function attachMerchantPHIDs(array $merchant_phids) { $this->merchantPHIDs = $merchant_phids; return $this; diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 2b121a3b0c..f551cbb5fb 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -3,7 +3,8 @@ final class PhortuneCart extends PhortuneDAO implements PhabricatorApplicationTransactionInterface, - PhabricatorPolicyInterface { + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface { const STATUS_BUILDING = 'cart:building'; const STATUS_READY = 'cart:ready'; @@ -652,26 +653,15 @@ final class PhortuneCart extends PhortuneDAO } public function getPolicy($capability) { - // NOTE: Both view and edit use the account's edit policy. We punch a hole - // through this for merchants, below. - return $this - ->getAccount() - ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { - return true; - } - - // If the viewer controls the merchant this order was placed with, they - // can view the order. - if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - $can_admin = PhabricatorPolicyFilter::hasCapability( - $viewer, - $this->getMerchant(), - PhabricatorPolicyCapability::CAN_EDIT); - if ($can_admin) { + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { return true; } } @@ -679,10 +669,20 @@ final class PhortuneCart extends PhortuneDAO return false; } - public function describeAutomaticCapability($capability) { + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + return array( - pht('Orders inherit the policies of the associated account.'), - pht('The merchant you placed an order with can review and manage it.'), + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), ); } diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php index da199d0751..41808a31e5 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -7,7 +7,9 @@ * charge followed by a successful charge. */ final class PhortuneCharge extends PhortuneDAO - implements PhabricatorPolicyInterface { + implements + PhabricatorPolicyInterface, + PhabricatorExtendedPolicyInterface { const STATUS_CHARGING = 'charge:charging'; const STATUS_CHARGED = 'charge:charged'; @@ -162,19 +164,42 @@ final class PhortuneCharge extends PhortuneDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { - return $this->getAccount()->getPolicy($capability); + return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return $this->getAccount()->hasAutomaticCapability($capability, $viewer); + if ($capability === PhabricatorPolicyCapability::CAN_VIEW) { + $any_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + if ($any_edit) { + return true; + } + } + + return false; } - public function describeAutomaticCapability($capability) { - return pht('Charges inherit the policies of the associated account.'); + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + if ($this->hasAutomaticCapability($capability, $viewer)) { + return array(); + } + + return array( + array( + $this->getAccount(), + PhabricatorPolicyCapability::CAN_EDIT, + ), + ); } } diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php index 260b79a257..84fa23a812 100644 --- a/src/applications/phortune/storage/PhortuneSubscription.php +++ b/src/applications/phortune/storage/PhortuneSubscription.php @@ -189,10 +189,10 @@ final class PhortuneSubscription } public function getURI() { - $account_id = $this->getAccount()->getID(); - $id = $this->getID(); - - return "/phortune/{$account_id}/subscription/view/{$id}/"; + return urisprintf( + '/phortune/account/%d/subscriptions/%d/', + $this->getAccount()->getID(), + $this->getID()); } public function getEditURI() { From 9bcd683c084a27853a5ad3caae292a6bc294beae Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Aug 2019 16:58:12 -0700 Subject: [PATCH 110/122] Update Phortune Merchant UI to bring it in line with Account UI Summary: Depends on D20732. Ref T13366. This generally makes the "Merchant" UI look and work like the "Payment Account" UI. This is mostly simpler since the permissions have largely been sorted out already and there's less going on here and less weirdness around view/edit policies. Test Plan: Browsed all Merchant functions as a merchant member and non-member. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20733 --- .../autopatches/20190822.merchant.01.view.sql | 2 + src/__phutil_library_map__.php | 44 ++- .../PhabricatorPhortuneApplication.php | 50 +-- .../controller/PhortuneLandingController.php | 2 +- .../PhortuneAccountEmailViewController.php | 1 - .../account/PhortuneAccountListController.php | 2 +- .../PhortuneAccountOrderListController.php | 5 +- .../PhortuneAccountOverviewController.php | 13 +- .../PhortuneAccountProfileController.php | 12 +- ...rtuneAccountSubscriptionViewController.php | 25 +- .../cart/PhortuneCartListController.php | 134 -------- .../PhortuneMerchantAddManagerController.php | 37 +- .../merchant/PhortuneMerchantController.php | 81 ++++- .../PhortuneMerchantDetailsController.php | 151 ++++++++ .../PhortuneMerchantEditController.php | 2 +- ...hortuneMerchantInvoiceCreateController.php | 11 +- .../PhortuneMerchantListController.php | 33 +- ...=> PhortuneMerchantManagersController.php} | 43 ++- .../PhortuneMerchantOrderListController.php | 55 +++ .../PhortuneMerchantOrdersController.php | 78 +++++ .../PhortuneMerchantOverviewController.php | 136 ++++++++ .../PhortuneMerchantPictureController.php | 30 +- .../PhortuneMerchantProfileController.php | 91 ++--- ...tuneMerchantProviderDisableController.php} | 15 +- ...hortuneMerchantProviderEditController.php} | 47 ++- ...PhortuneMerchantProviderViewController.php | 127 +++++++ .../PhortuneMerchantProvidersController.php | 116 +++++++ ...tuneMerchantSubscriptionListController.php | 50 +++ ...hortuneMerchantSubscriptionsController.php | 68 ++++ .../PhortuneMerchantViewController.php | 324 ------------------ .../editor/PhortuneMerchantEditEngine.php | 2 +- .../editor/PhortuneMerchantEditor.php | 1 - .../provider/PhortuneTestPaymentProvider.php | 2 +- .../phortune/storage/PhortuneMerchant.php | 47 ++- .../storage/PhortunePaymentProviderConfig.php | 12 + .../phortune/view/PhortuneOrderTableView.php | 22 +- 36 files changed, 1122 insertions(+), 749 deletions(-) create mode 100644 resources/sql/autopatches/20190822.merchant.01.view.sql delete mode 100644 src/applications/phortune/controller/cart/PhortuneCartListController.php create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php rename src/applications/phortune/controller/merchant/{PhortuneMerchantManagerController.php => PhortuneMerchantManagersController.php} (73%) create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php rename src/applications/phortune/controller/{provider/PhortuneProviderDisableController.php => merchant/PhortuneMerchantProviderDisableController.php} (84%) rename src/applications/phortune/controller/{provider/PhortuneProviderEditController.php => merchant/PhortuneMerchantProviderEditController.php} (87%) create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php create mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php delete mode 100644 src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php diff --git a/resources/sql/autopatches/20190822.merchant.01.view.sql b/resources/sql/autopatches/20190822.merchant.01.view.sql new file mode 100644 index 0000000000..cb609f054e --- /dev/null +++ b/resources/sql/autopatches/20190822.merchant.01.view.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_merchant + DROP viewPolicy; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4f00d449fa..5fee952d0a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5271,7 +5271,6 @@ phutil_register_library_map(array( 'PhortuneCartController' => 'applications/phortune/controller/cart/PhortuneCartController.php', 'PhortuneCartEditor' => 'applications/phortune/editor/PhortuneCartEditor.php', 'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php', - 'PhortuneCartListController' => 'applications/phortune/controller/cart/PhortuneCartListController.php', 'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php', 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', 'PhortuneCartReplyHandler' => 'applications/phortune/mail/PhortuneCartReplyHandler.php', @@ -5304,6 +5303,7 @@ phutil_register_library_map(array( 'PhortuneMerchantContactInfoTransaction' => 'applications/phortune/xaction/PhortuneMerchantContactInfoTransaction.php', 'PhortuneMerchantController' => 'applications/phortune/controller/merchant/PhortuneMerchantController.php', 'PhortuneMerchantDescriptionTransaction' => 'applications/phortune/xaction/PhortuneMerchantDescriptionTransaction.php', + 'PhortuneMerchantDetailsController' => 'applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php', 'PhortuneMerchantEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantEditController.php', 'PhortuneMerchantEditEngine' => 'applications/phortune/editor/PhortuneMerchantEditEngine.php', 'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php', @@ -5313,18 +5313,26 @@ phutil_register_library_map(array( 'PhortuneMerchantInvoiceEmailTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceEmailTransaction.php', 'PhortuneMerchantInvoiceFooterTransaction' => 'applications/phortune/xaction/PhortuneMerchantInvoiceFooterTransaction.php', 'PhortuneMerchantListController' => 'applications/phortune/controller/merchant/PhortuneMerchantListController.php', - 'PhortuneMerchantManagerController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagerController.php', + 'PhortuneMerchantManagersController' => 'applications/phortune/controller/merchant/PhortuneMerchantManagersController.php', 'PhortuneMerchantNameTransaction' => 'applications/phortune/xaction/PhortuneMerchantNameTransaction.php', + 'PhortuneMerchantOrderListController' => 'applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php', + 'PhortuneMerchantOrdersController' => 'applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php', + 'PhortuneMerchantOverviewController' => 'applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php', 'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php', 'PhortuneMerchantPictureController' => 'applications/phortune/controller/merchant/PhortuneMerchantPictureController.php', 'PhortuneMerchantPictureTransaction' => 'applications/phortune/xaction/PhortuneMerchantPictureTransaction.php', 'PhortuneMerchantProfileController' => 'applications/phortune/controller/merchant/PhortuneMerchantProfileController.php', + 'PhortuneMerchantProviderDisableController' => 'applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php', + 'PhortuneMerchantProviderEditController' => 'applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php', + 'PhortuneMerchantProviderViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php', + 'PhortuneMerchantProvidersController' => 'applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php', 'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php', 'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php', + 'PhortuneMerchantSubscriptionListController' => 'applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php', + 'PhortuneMerchantSubscriptionsController' => 'applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php', 'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php', 'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php', 'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php', - 'PhortuneMerchantViewController' => 'applications/phortune/controller/merchant/PhortuneMerchantViewController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php', 'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php', @@ -5356,8 +5364,6 @@ phutil_register_library_map(array( 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php', 'PhortuneProductViewController' => 'applications/phortune/controller/product/PhortuneProductViewController.php', 'PhortuneProviderActionController' => 'applications/phortune/controller/provider/PhortuneProviderActionController.php', - 'PhortuneProviderDisableController' => 'applications/phortune/controller/provider/PhortuneProviderDisableController.php', - 'PhortuneProviderEditController' => 'applications/phortune/controller/provider/PhortuneProviderEditController.php', 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php', 'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php', @@ -11847,7 +11853,6 @@ phutil_register_library_map(array( 'PhortuneCartController' => 'PhortuneController', 'PhortuneCartEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneCartImplementation' => 'Phobject', - 'PhortuneCartListController' => 'PhortuneController', 'PhortuneCartPHIDType' => 'PhabricatorPHIDType', 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneCartReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', @@ -11883,32 +11888,41 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), - 'PhortuneMerchantAddManagerController' => 'PhortuneController', + 'PhortuneMerchantAddManagerController' => 'PhortuneMerchantController', 'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability', 'PhortuneMerchantContactInfoTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantController' => 'PhortuneController', 'PhortuneMerchantDescriptionTransaction' => 'PhortuneMerchantTransactionType', - 'PhortuneMerchantEditController' => 'PhortuneMerchantController', + 'PhortuneMerchantDetailsController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantEditController' => 'PhortuneController', 'PhortuneMerchantEditEngine' => 'PhabricatorEditEngine', 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneMerchantHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType', - 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController', 'PhortuneMerchantInvoiceEmailTransaction' => 'PhortuneMerchantTransactionType', 'PhortuneMerchantInvoiceFooterTransaction' => 'PhortuneMerchantTransactionType', - 'PhortuneMerchantListController' => 'PhortuneMerchantController', - 'PhortuneMerchantManagerController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantListController' => 'PhortuneController', + 'PhortuneMerchantManagersController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantNameTransaction' => 'PhortuneMerchantTransactionType', + 'PhortuneMerchantOrderListController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantOrdersController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantOverviewController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType', - 'PhortuneMerchantPictureController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantPictureController' => 'PhortuneMerchantController', 'PhortuneMerchantPictureTransaction' => 'PhortuneMerchantTransactionType', - 'PhortuneMerchantProfileController' => 'PhortuneController', + 'PhortuneMerchantProfileController' => 'PhortuneMerchantController', + 'PhortuneMerchantProviderDisableController' => 'PhortuneMerchantController', + 'PhortuneMerchantProviderEditController' => 'PhortuneMerchantController', + 'PhortuneMerchantProviderViewController' => 'PhortuneMerchantController', + 'PhortuneMerchantProvidersController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhortuneMerchantSubscriptionListController' => 'PhortuneMerchantProfileController', + 'PhortuneMerchantSubscriptionsController' => 'PhortuneMerchantProfileController', 'PhortuneMerchantTransaction' => 'PhabricatorModularTransaction', 'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType', - 'PhortuneMerchantViewController' => 'PhortuneMerchantProfileController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneOrderTableView' => 'AphrontView', 'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider', @@ -11953,8 +11967,6 @@ phutil_register_library_map(array( 'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneProductViewController' => 'PhortuneController', 'PhortuneProviderActionController' => 'PhortuneController', - 'PhortuneProviderDisableController' => 'PhortuneMerchantController', - 'PhortuneProviderEditController' => 'PhortuneMerchantController', 'PhortunePurchase' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 20dbc5c88f..15eb500fc3 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -103,36 +103,40 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { => 'PhortuneProviderActionController', ), 'merchant/' => array( - '(?:query/(?P[^/]+)/)?' => 'PhortuneMerchantListController', - 'picture/(?:(?P\d+)/)?' => 'PhortuneMerchantPictureController', + $this->getQueryRoutePattern() + => 'PhortuneMerchantListController', $this->getEditRoutePattern('edit/') => 'PhortuneMerchantEditController', - 'orders/(?P\d+)/(?:query/(?P[^/]+)/)?' - => 'PhortuneCartListController', - 'manager/' => array( - '(?:(?P\d+)/)?' => 'PhortuneMerchantManagerController', - 'add/(?:(?P\d+)/)?' => 'PhortuneMerchantAddManagerController', - ), '(?P\d+)/' => array( - 'cart/(?P\d+)/' => array( - '' => 'PhortuneCartViewController', - '(?Pcancel|refund)/' => 'PhortuneCartCancelController', - 'update/' => 'PhortuneCartUpdateController', - 'accept/' => 'PhortuneCartAcceptController', + '' => 'PhortuneMerchantOverviewController', + 'details/' => 'PhortuneMerchantDetailsController', + 'providers/' => array( + '' => 'PhortuneMerchantProvidersController', + '(?P\d+)/' => array( + '' => 'PhortuneMerchantProviderViewController', + 'disable/' => 'PhortuneMerchantProviderDisableController', + ), + $this->getEditRoutePattern('edit/') + => 'PhortuneMerchantProviderEditController', ), - 'subscription/' => array( - '(?:query/(?P[^/]+)/)?' - => 'PhortuneSubscriptionListController', - 'view/(?P\d+)/' - => 'PhortuneAccountSubscriptionViewController', - 'order/(?P\d+)/' - => 'PhortuneCartListController', + 'orders/' => array( + '' => 'PhortuneMerchantOrdersController', + $this->getQueryRoutePattern('list/') + => 'PhortuneMerchantOrderListController', ), - 'invoice/' => array( - 'new/' => 'PhortuneMerchantInvoiceCreateController', + 'picture/' => array( + 'edit/' => 'PhortuneMerchantPictureController', + ), + 'subscriptions/' => array( + '' => 'PhortuneMerchantSubscriptionsController', + $this->getQueryRoutePattern('list/') + => 'PhortuneMerchantSubscriptionListController', + ), + 'managers/' => array( + '' => 'PhortuneMerchantManagersController', + 'new/' => 'PhortuneMerchantAddManagerController', ), ), - '(?P\d+)/' => 'PhortuneMerchantViewController', ), ), ); diff --git a/src/applications/phortune/controller/PhortuneLandingController.php b/src/applications/phortune/controller/PhortuneLandingController.php index e6906095d2..e20d222a86 100644 --- a/src/applications/phortune/controller/PhortuneLandingController.php +++ b/src/applications/phortune/controller/PhortuneLandingController.php @@ -11,7 +11,7 @@ final class PhortuneLandingController extends PhortuneController { if (count($accounts) == 1) { $account = head($accounts); - $next_uri = $this->getApplicationURI($account->getID().'/'); + $next_uri = $account->getURI(); } else { $next_uri = $this->getApplicationURI('account/'); } diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php index 7732518d72..14c2b842f9 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -50,7 +50,6 @@ final class PhortuneAccountEmailViewController ->setTitle($address->getObjectName()) ->setCrumbs($crumbs) ->appendChild($view); - } private function buildCurtainView(PhortuneAccountEmail $address) { diff --git a/src/applications/phortune/controller/account/PhortuneAccountListController.php b/src/applications/phortune/controller/account/PhortuneAccountListController.php index 177f84d75d..0ac8cecd8b 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountListController.php @@ -38,7 +38,7 @@ final class PhortuneAccountListController extends PhortuneController { $item = id(new PHUIObjectItemView()) ->setSubhead(pht('Account %d', $account->getID())) ->setHeader($account->getName()) - ->setHref($this->getApplicationURI($account->getID().'/')) + ->setHref($account->getURI()) ->setObject($account) ->setImageIcon('fa-user-circle'); diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php b/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php index fd4a3c389f..afdfc9fcce 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountOrderListController.php @@ -44,11 +44,8 @@ final class PhortuneAccountOrderListController $subscription->getURI()); } else if ($this->hasAccount()) { $account = $this->getAccount(); - $id = $account->getID(); - $crumbs->addTextCrumb( - pht('Orders'), - $account->getOrdersURI()); + $crumbs->addTextCrumb(pht('Orders'), $account->getOrdersURI()); } return $crumbs; diff --git a/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php index 078599faca..3b8e8497db 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountOverviewController.php @@ -66,22 +66,11 @@ final class PhortuneAccountOverviewController $viewer = $this->getViewer(); - $phids = array(); - foreach ($carts as $cart) { - $phids[] = $cart->getPHID(); - $phids[] = $cart->getMerchantPHID(); - foreach ($cart->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - $table = id(new PhortuneOrderTableView()) ->setNoDataString(pht('You have no unpaid invoices.')) ->setIsInvoices(true) ->setUser($viewer) - ->setCarts($carts) - ->setHandles($handles); + ->setCarts($carts); $header = id(new PHUIHeaderView()) ->setHeader(pht('Invoices Due')); diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 739316805f..33361c1f66 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -117,21 +117,11 @@ abstract class PhortuneAccountProfileController ->setLimit($limit) ->execute(); - $phids = array(); - foreach ($carts as $cart) { - $phids[] = $cart->getPHID(); - foreach ($cart->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - $orders_uri = $account->getOrderListURI(); $table = id(new PhortuneOrderTableView()) ->setUser($viewer) - ->setCarts($carts) - ->setHandles($handles); + ->setCarts($carts); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Orders')) diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php index c3e39b94d7..e38c18779b 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php @@ -92,21 +92,10 @@ final class PhortuneAccountSubscriptionViewController ->withInvoices(true) ->execute(); - $phids = array(); - foreach ($invoices as $invoice) { - $phids[] = $invoice->getPHID(); - $phids[] = $invoice->getMerchantPHID(); - foreach ($invoice->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - $invoice_table = id(new PhortuneOrderTableView()) ->setUser($viewer) ->setCarts($invoices) - ->setIsInvoices(true) - ->setHandles($handles); + ->setIsInvoices(true); $invoice_header = id(new PHUIHeaderView()) ->setHeader(pht('Invoices Due')); @@ -135,19 +124,9 @@ final class PhortuneAccountSubscriptionViewController ->setLimit(50) ->execute(); - $phids = array(); - foreach ($invoices as $invoice) { - $phids[] = $invoice->getPHID(); - foreach ($invoice->getPurchases() as $purchase) { - $phids[] = $purchase->getPHID(); - } - } - $handles = $this->loadViewerHandles($phids); - $invoice_table = id(new PhortuneOrderTableView()) ->setUser($viewer) - ->setCarts($invoices) - ->setHandles($handles); + ->setCarts($invoices); $account = $subscription->getAccount(); $merchant = $subscription->getMerchant(); diff --git a/src/applications/phortune/controller/cart/PhortuneCartListController.php b/src/applications/phortune/controller/cart/PhortuneCartListController.php deleted file mode 100644 index 0537417f7c..0000000000 --- a/src/applications/phortune/controller/cart/PhortuneCartListController.php +++ /dev/null @@ -1,134 +0,0 @@ -getViewer(); - - $merchant_id = $request->getURIData('merchantID'); - $account_id = $request->getURIData('accountID'); - $subscription_id = $request->getURIData('subscriptionID'); - - $engine = id(new PhortuneCartSearchEngine()) - ->setViewer($viewer); - - if ($merchant_id) { - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($merchant_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - $this->merchant = $merchant; - $viewer->grantAuthority($merchant); - $engine->setMerchant($merchant); - } else if ($account_id) { - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $this->account = $account; - $engine->setAccount($account); - } else { - return new Aphront404Response(); - } - - // NOTE: We must process this after processing the merchant authority, so - // it becomes visible in merchant contexts. - if ($subscription_id) { - $subscription = id(new PhortuneSubscriptionQuery()) - ->setViewer($viewer) - ->withIDs(array($subscription_id)) - ->executeOne(); - if (!$subscription) { - return new Aphront404Response(); - } - $this->subscription = $subscription; - $engine->setSubscription($subscription); - } - - $this->engine = $engine; - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine($engine) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getRequest()->getUser(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - $this->engine->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $subscription = $this->subscription; - - $merchant = $this->merchant; - if ($merchant) { - $id = $merchant->getID(); - $this->addMerchantCrumb($crumbs, $merchant); - if (!$subscription) { - $crumbs->addTextCrumb( - pht('Orders'), - $this->getApplicationURI("merchant/orders/{$id}/")); - } - } - - $account = $this->account; - if ($account) { - $id = $account->getID(); - $this->addAccountCrumb($crumbs, $account); - if (!$subscription) { - $crumbs->addTextCrumb( - pht('Orders'), - $this->getApplicationURI("{$id}/order/")); - } - } - - if ($subscription) { - if ($merchant) { - $subscription_uri = $subscription->getMerchantURI(); - } else { - $subscription_uri = $subscription->getURI(); - } - $crumbs->addTextCrumb( - $subscription->getSubscriptionName(), - $subscription_uri); - } - - return $crumbs; - } - -} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php index 3ef0a53874..0f4bb479e1 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantAddManagerController.php @@ -1,32 +1,23 @@ getViewer(); - $id = $request->getURIData('id'); - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } + $merchant = $this->getMerchant(); $v_members = array(); $e_members = null; - $merchant_uri = $this->getApplicationURI("/merchant/manager/{$id}/"); + $merchant_uri = $merchant->getManagersURI(); - if ($request->isFormPost()) { + if ($request->isFormOrHiSecPost()) { $xactions = array(); - $v_members = $request->getArr('memberPHIDs'); + $v_members = $request->getArr('managerPHIDs'); $type_edge = PhabricatorTransactions::TYPE_EDGE; $xactions[] = id(new PhortuneMerchantTransaction()) @@ -59,13 +50,13 @@ final class PhortuneMerchantAddManagerController extends PhortuneController { ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) - ->setLabel(pht('Members')) - ->setName('memberPHIDs') + ->setLabel(pht('New Managers')) + ->setName('managerPHIDs') ->setValue($v_members) ->setError($e_members)); return $this->newDialog() - ->setTitle(pht('Add New Manager')) + ->setTitle(pht('Add New Managers')) ->appendForm($form) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addCancelButton($merchant_uri) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantController.php index 23b96197a0..15c4e0047a 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantController.php @@ -3,11 +3,80 @@ abstract class PhortuneMerchantController extends PhortuneController { - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - $crumbs->addTextCrumb( - pht('Merchants'), - $this->getApplicationURI('merchant/')); - return $crumbs; + private $merchant; + + final protected function setMerchant(PhortuneMerchant $merchant) { + $this->merchant = $merchant; + return $this; } + + final protected function getMerchant() { + return $this->merchant; + } + + final protected function hasMerchant() { + return (bool)$this->merchant; + } + + final public function handleRequest(AphrontRequest $request) { + if ($this->shouldRequireMerchantEditCapability()) { + $response = $this->loadMerchantForEdit(); + } else { + $response = $this->loadMerchantForView(); + } + + if ($response) { + return $response; + } + + return $this->handleMerchantRequest($request); + } + + abstract protected function shouldRequireMerchantEditCapability(); + abstract protected function handleMerchantRequest(AphrontRequest $request); + + private function loadMerchantForEdit() { + return $this->loadMerchantWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + } + + private function loadMerchantForView() { + return $this->loadMerchantWithCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + } + + private function loadMerchantWithCapabilities(array $capabilities) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + $merchant_id = $request->getURIData('merchantID'); + if (!$merchant_id) { + throw new Exception( + pht( + 'Controller ("%s") extends controller "%s", but is reachable '. + 'with no "merchantID" in URI.', + get_class($this), + __CLASS__)); + } + + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($merchant_id)) + ->needProfileImage(true) + ->requireCapabilities($capabilities) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + + $this->setMerchant($merchant); + + return null; + } + } diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php new file mode 100644 index 0000000000..8b558ac387 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantDetailsController.php @@ -0,0 +1,151 @@ +getViewer(); + $id = $request->getURIData('id'); + + $merchant = $this->getMerchant(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Account Details')) + ->setBorder(true); + $header = $this->buildHeaderView(); + + $title = pht( + '%s %s', + $merchant->getObjectName(), + $merchant->getName()); + + $details = $this->buildDetailsView($merchant); + $curtain = $this->buildCurtainView($merchant); + + $timeline = $this->buildTransactionTimeline( + $merchant, + new PhortuneMerchantTransactionQuery()); + $timeline->setShouldTerminate(true); + + $navigation = $this->buildSideNavView('details'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $details, + $timeline, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildDetailsView(PhortuneMerchant $merchant) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($merchant); + + $invoice_from = $merchant->getInvoiceEmail(); + if (!$invoice_from) { + $invoice_from = pht('No email address set'); + $invoice_from = phutil_tag('em', array(), $invoice_from); + } + $view->addProperty(pht('Invoice From'), $invoice_from); + + $description = $merchant->getDescription(); + if (strlen($description)) { + $description = new PHUIRemarkupView($viewer, $description); + $view->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($description); + } + + $contact_info = $merchant->getContactInfo(); + if (strlen($contact_info)) { + $contact_info = new PHUIRemarkupView($viewer, $contact_info); + $view->addSectionHeader( + pht('Contact Information'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($contact_info); + } + + $footer_info = $merchant->getInvoiceFooter(); + if (strlen($footer_info)) { + $footer_info = new PHUIRemarkupView($viewer, $footer_info); + $view->addSectionHeader( + pht('Invoice Footer'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($footer_info); + } + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($view); + } + + private function buildCurtainView(PhortuneMerchant $merchant) { + $viewer = $this->getRequest()->getUser(); + $id = $merchant->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $merchant, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($merchant); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Merchant')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($this->getApplicationURI("merchant/edit/{$id}/"))); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Logo')) + ->setIcon('fa-picture-o') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($this->getApplicationURI("merchant/{$id}/picture/edit/"))); + + $member_phids = $merchant->getMemberPHIDs(); + $handles = $viewer->loadHandles($member_phids); + + $member_list = id(new PHUIObjectItemListView()) + ->setSimple(true); + + foreach ($member_phids as $member_phid) { + $image_uri = $handles[$member_phid]->getImageURI(); + $image_href = $handles[$member_phid]->getURI(); + $person = $handles[$member_phid]; + + $member = id(new PHUIObjectItemView()) + ->setImageURI($image_uri) + ->setHref($image_href) + ->setHeader($person->getFullName()); + + $member_list->addItem($member); + } + + $curtain->newPanel() + ->setHeaderText(pht('Managers')) + ->appendChild($member_list); + + return $curtain; + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php index 07c743251c..78125d43d6 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantEditController.php @@ -1,7 +1,7 @@ getUser(); $merchant = $this->loadMerchantAuthority(); diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php index 48fb02195b..87cfaaf880 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantListController.php @@ -1,37 +1,12 @@ getViewer(); - $querykey = $request->getURIData('queryKey'); - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine(new PhortuneMerchantSearchEngine()) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getViewer(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhortuneMerchantSearchEngine()) - ->setViewer($viewer) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; + return id(new PhortuneMerchantSearchEngine()) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantManagerController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantManagersController.php similarity index 73% rename from src/applications/phortune/controller/merchant/PhortuneMerchantManagerController.php rename to src/applications/phortune/controller/merchant/PhortuneMerchantManagersController.php index 0b9c0c6598..6b3ff568ff 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantManagerController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantManagersController.php @@ -1,35 +1,29 @@ getViewer(); - $id = $request->getURIData('id'); + $merchant = $this->getMerchant(); - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $this->setMerchant($merchant); - $header = $this->buildHeaderView(); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Managers')); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Managers')) + ->setBorder(true); $header = $this->buildHeaderView(); $members = $this->buildMembersSection($merchant); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $members, - )); + ->setFooter( + array( + $members, + )); $navigation = $this->buildSideNavView('managers'); @@ -38,7 +32,6 @@ final class PhortuneMerchantManagerController ->setCrumbs($crumbs) ->setNavigation($navigation) ->appendChild($view); - } private function buildMembersSection(PhortuneMerchant $merchant) { @@ -51,12 +44,18 @@ final class PhortuneMerchantManagerController $id = $merchant->getID(); + $add_uri = urisprintf( + 'merchant/%d/managers/new/', + $merchant->getID()); + $add_uri = $this->getApplicationURI($add_uri); + $add = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('New Manager')) ->setIcon('fa-plus') ->setWorkflow(true) - ->setHref("/phortune/merchant/manager/add/{$id}/"); + ->setDisabled(!$can_edit) + ->setHref($add_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Merchant Account Managers')) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php new file mode 100644 index 0000000000..5d33564ba7 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantOrderListController.php @@ -0,0 +1,55 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $engine = id(new PhortuneCartSearchEngine()) + ->setController($this) + ->setMerchant($merchant); + + $subscription_id = $request->getURIData('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $engine->setSubscription($subscription); + $this->subscription = $subscription; + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $subscription = $this->subscription; + if ($subscription) { + $crumbs->addTextCrumb( + $subscription->getObjectName(), + $subscription->getURI()); + } else if ($this->hasMerchant()) { + $merchant = $this->getMerchant(); + + $crumbs->addTextCrumb(pht('Orders'), $merchant->getOrdersURI()); + } + + return $crumbs; + } + + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php new file mode 100644 index 0000000000..4f84d0cce7 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantOrdersController.php @@ -0,0 +1,78 @@ +getMerchant(); + $title = $merchant->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Orders')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + $order_history = $this->newRecentOrdersView($merchant, 100); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $order_history, + )); + + $navigation = $this->buildSideNavView('orders'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function newRecentOrdersView( + PhortuneMerchant $merchant, + $limit) { + + $viewer = $this->getViewer(); + + $carts = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->needPurchases(true) + ->withStatuses( + array( + PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, + PhortuneCart::STATUS_PURCHASED, + )) + ->setLimit($limit) + ->execute(); + + $orders_uri = $merchant->getOrderListURI(); + + $table = id(new PhortuneOrderTableView()) + ->setUser($viewer) + ->setCarts($carts); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Orders')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($orders_uri) + ->setText(pht('View All Orders'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php new file mode 100644 index 0000000000..2925a7d34b --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantOverviewController.php @@ -0,0 +1,136 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $crumbs = $this->buildApplicationCrumbs() + ->setBorder(true); + + $header = $this->buildHeaderView(); + + $title = pht( + '%s %s', + $merchant->getObjectName(), + $merchant->getName()); + + $providers = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + + $details = $this->buildDetailsView($merchant, $providers); + $navigation = $this->buildSideNavView('overview'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $details, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildDetailsView( + PhortuneMerchant $merchant, + array $providers) { + + $viewer = $this->getRequest()->getUser(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($merchant); + + $status_view = new PHUIStatusListView(); + + $have_any = false; + $any_test = false; + foreach ($providers as $provider_config) { + $provider = $provider_config->buildProvider(); + if ($provider->isEnabled()) { + $have_any = true; + } + if (!$provider->isAcceptingLivePayments()) { + $any_test = true; + } + } + + if ($have_any) { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') + ->setTarget(pht('Accepts Payments')) + ->setNote(pht('This merchant can accept payments.'))); + + if ($any_test) { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') + ->setTarget(pht('Test Mode')) + ->setNote(pht('This merchant is accepting test payments.'))); + } else { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') + ->setTarget(pht('Live Mode')) + ->setNote(pht('This merchant is accepting live payments.'))); + } + } else if ($providers) { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_REJECT, 'red') + ->setTarget(pht('No Enabled Providers')) + ->setNote( + pht( + 'All of the payment providers for this merchant are '. + 'disabled.'))); + } else { + $status_view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') + ->setTarget(pht('No Providers')) + ->setNote( + pht( + 'This merchant does not have any payment providers configured '. + 'yet, so it can not accept payments. Add a provider.'))); + } + + $view->addProperty(pht('Status'), $status_view); + + $description = $merchant->getDescription(); + if (strlen($description)) { + $description = new PHUIRemarkupView($viewer, $description); + $view->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($description); + } + + $contact_info = $merchant->getContactInfo(); + if (strlen($contact_info)) { + $contact_info = new PHUIRemarkupView($viewer, $contact_info); + $view->addSectionHeader( + pht('Contact Information'), + PHUIPropertyListView::ICON_SUMMARY); + $view->addTextContent($contact_info); + } + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($view); + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php index cbd122f682..96f4f8e367 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantPictureController.php @@ -1,28 +1,17 @@ getViewer(); - $id = $request->getURIData('id'); + $merchant = $this->getMerchant(); - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $this->setMerchant($merchant); - $uri = $merchant->getURI(); + $uri = $merchant->getDetailsURI(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; @@ -222,12 +211,9 @@ final class PhortuneMerchantPictureController $upload_box, )); - $navigation = $this->buildSideNavView(); - return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->setNavigation($navigation) ->appendChild( array( $view, diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php index 45911bfef9..bc54756ffe 100644 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProfileController.php @@ -1,22 +1,7 @@ merchant = $merchant; - return $this; - } - - public function getMerchant() { - return $this->merchant; - } - - public function buildApplicationMenu() { - return $this->buildSideNavView()->getMenu(); - } + extends PhortuneMerchantController { protected function buildHeaderView() { $viewer = $this->getViewer(); @@ -26,20 +11,20 @@ abstract class PhortuneMerchantProfileController $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) - ->setPolicyObject($merchant) ->setImage($merchant->getProfileImageURI()); return $header; } protected function buildApplicationCrumbs() { - $merchant = $this->getMerchant(); - $id = $merchant->getID(); - $merchant_uri = $this->getApplicationURI("/merchant/{$id}/"); - $crumbs = parent::buildApplicationCrumbs(); - $crumbs->addTextCrumb($merchant->getName(), $merchant_uri); - $crumbs->setBorder(true); + + if ($this->hasMerchant()) { + $merchant = $this->getMerchant(); + $merchant_uri = $merchant->getURI(); + $crumbs->addTextCrumb($merchant->getName(), $merchant_uri); + } + return $crumbs; } @@ -58,31 +43,47 @@ abstract class PhortuneMerchantProfileController $nav->addLabel(pht('Merchant')); - $nav->addFilter( - 'overview', - pht('Overview'), - $this->getApplicationURI("/merchant/{$id}/"), - 'fa-building-o'); + $nav->newLink('overview') + ->setName(pht('Overview')) + ->setHref($merchant->getURI()) + ->setIcon('fa-building-o'); - if ($can_edit) { - $nav->addFilter( - 'orders', - pht('Orders'), - $this->getApplicationURI("merchant/orders/{$id}/"), - 'fa-retweet'); + $nav->newLink('details') + ->setName(pht('Account Details')) + ->setHref($merchant->getDetailsURI()) + ->setIcon('fa-address-card-o') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); - $nav->addFilter( - 'subscriptions', - pht('Subscriptions'), - $this->getApplicationURI("merchant/{$id}/subscription/"), - 'fa-shopping-cart'); + $nav->addLabel(pht('Payments')); - $nav->addFilter( - 'managers', - pht('Managers'), - $this->getApplicationURI("/merchant/manager/{$id}/"), - 'fa-group'); - } + $nav->newLink('providers') + ->setName(pht('Payment Providers')) + ->setHref($merchant->getPaymentProvidersURI()) + ->setIcon('fa-credit-card') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $nav->newLink('orders') + ->setName(pht('Orders')) + ->setHref($merchant->getOrdersURI()) + ->setIcon('fa-shopping-bag') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $nav->newLink('subscriptions') + ->setName(pht('Subscriptions')) + ->setHref($merchant->getSubscriptionsURI()) + ->setIcon('fa-retweet') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $nav->addLabel(pht('Personnel')); + + $nav->newLink('managers') + ->setName(pht('Managers')) + ->setHref($merchant->getManagersURI()) + ->setIcon('fa-group'); $nav->selectFilter($filter); diff --git a/src/applications/phortune/controller/provider/PhortuneProviderDisableController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php similarity index 84% rename from src/applications/phortune/controller/provider/PhortuneProviderDisableController.php rename to src/applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php index 03236b54bc..01928fca1b 100644 --- a/src/applications/phortune/controller/provider/PhortuneProviderDisableController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderDisableController.php @@ -1,11 +1,17 @@ getViewer(); - $id = $request->getURIData('id'); + $merchant = $this->getMerchant(); + + $id = $request->getURIData('providerID'); $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) @@ -20,9 +26,8 @@ final class PhortuneProviderDisableController return new Aphront404Response(); } - $merchant = $provider_config->getMerchant(); $merchant_id = $merchant->getID(); - $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); + $cancel_uri = $provider_config->getURI(); $provider = $provider_config->buildProvider(); diff --git a/src/applications/phortune/controller/provider/PhortuneProviderEditController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php similarity index 87% rename from src/applications/phortune/controller/provider/PhortuneProviderEditController.php rename to src/applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php index f7ad2486c4..bd577ecfb5 100644 --- a/src/applications/phortune/controller/provider/PhortuneProviderEditController.php +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderEditController.php @@ -1,16 +1,23 @@ getViewer(); + $merchant = $this->getMerchant(); + $id = $request->getURIData('id'); if ($id) { $provider_config = id(new PhortunePaymentProviderConfigQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->withMerchantPHIDs(array($merchant->getPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -25,20 +32,8 @@ final class PhortuneProviderEditController $merchant = $provider_config->getMerchant(); $merchant_id = $merchant->getID(); - $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); + $cancel_uri = $provider_config->getURI(); } else { - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getStr('merchantID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } $merchant_id = $merchant->getID(); $current_providers = id(new PhortunePaymentProviderConfigQuery()) @@ -62,9 +57,7 @@ final class PhortuneProviderEditController } $provider_config->setProviderClass($class); - - $cancel_uri = $this->getApplicationURI( - 'provider/edit/?merchantID='.$merchant_id); + $cancel_uri = $merchant->getPaymentProvidersURI(); } $provider = $provider_config->buildProvider(); @@ -123,10 +116,12 @@ final class PhortuneProviderEditController $xactions = array(); - $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) - ->setTransactionType( - PhortunePaymentProviderConfigTransaction::TYPE_CREATE) - ->setNewValue(true); + if (!$provider_config->getID()) { + $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) + ->setTransactionType( + PhortunePaymentProviderConfigTransaction::TYPE_CREATE) + ->setNewValue(true); + } foreach ($xaction_values as $key => $value) { $xactions[] = id(clone $template) @@ -143,9 +138,9 @@ final class PhortuneProviderEditController $editor->applyTransactions($provider_config, $xactions); - $merchant_uri = $this->getApplicationURI( - 'merchant/'.$merchant->getID().'/'); - return id(new AphrontRedirectResponse())->setURI($merchant_uri); + $next_uri = $provider_config->getURI(); + + return id(new AphrontRedirectResponse())->setURI($next_uri); } } } else { @@ -155,7 +150,6 @@ final class PhortuneProviderEditController $form = id(new AphrontFormView()) ->setUser($viewer) - ->addHiddenInput('merchantID', $merchant->getID()) ->addHiddenInput('class', $provider_config->getProviderClass()) ->addHiddenInput('edit', true) ->appendChild( @@ -261,7 +255,6 @@ final class PhortuneProviderEditController $form = id(new AphrontFormView()) ->setUser($viewer) - ->addHiddenInput('merchantID', $merchant->getID()) ->appendRemarkupInstructions( pht('Choose the type of payment provider to add:')) ->appendChild($panel_classes) diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php new file mode 100644 index 0000000000..2c1be9f8af --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProviderViewController.php @@ -0,0 +1,127 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $provider = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('providerID'))) + ->executeOne(); + if (!$provider) { + return new Aphront404Response(); + } + + $provider_type = $provider->buildProvider(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($merchant->getName(), $merchant->getURI()) + ->addTextCrumb( + pht('Payment Providers'), + $merchant->getPaymentProvidersURI()) + ->addTextCrumb($provider->getObjectName()) + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Provider: %s', $provider_type->getName())); + + $details = $this->newDetailsView($provider); + + $timeline = $this->buildTransactionTimeline( + $provider, + new PhortunePaymentProviderConfigTransactionQuery()); + $timeline->setShouldTerminate(true); + + $curtain = $this->buildCurtainView($provider); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $details, + $timeline, + )); + + return $this->newPage() + ->setTitle($provider->getObjectName()) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildCurtainView(PhortunePaymentProviderConfig $provider) { + $viewer = $this->getViewer(); + $merchant = $this->getMerchant(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $provider, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'merchant/%d/providers/edit/%d/', + $merchant->getID(), + $provider->getID())); + + $disable_uri = $this->getApplicationURI( + urisprintf( + 'merchant/%d/providers/%d/disable/', + $merchant->getID(), + $provider->getID())); + + $curtain = $this->newCurtainView($provider); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Provider')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + $provider_type = $provider->buildProvider(); + + if ($provider_type->isEnabled()) { + $disable_icon = 'fa-times'; + $disable_name = pht('Disable Provider'); + } else { + $disable_icon = 'fa-check'; + $disable_name = pht('Enable Provider'); + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + return $curtain; + } + + private function newDetailsView(PhortunePaymentProviderConfig $provider) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $provider_type = $provider->buildProvider(); + + $view->addProperty(pht('Provider Type'), $provider_type->getName()); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Payment Provider Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addPropertyList($view); + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php new file mode 100644 index 0000000000..2d64bdc726 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantProvidersController.php @@ -0,0 +1,116 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Payment Providers')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + + $title = pht( + '%s %s', + $merchant->getObjectName(), + $merchant->getName()); + + $providers = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + + $provider_list = $this->buildProviderList( + $merchant, + $providers); + + $navigation = $this->buildSideNavView('providers'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $provider_list, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildProviderList( + PhortuneMerchant $merchant, + array $providers) { + + $viewer = $this->getRequest()->getUser(); + $id = $merchant->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $merchant, + PhabricatorPolicyCapability::CAN_EDIT); + + $provider_list = id(new PHUIObjectItemListView()) + ->setNoDataString(pht('This merchant has no payment providers.')); + + foreach ($providers as $provider_config) { + $provider = $provider_config->buildProvider(); + $provider_id = $provider_config->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($provider_config->getObjectName()) + ->setHeader($provider->getName()) + ->setHref($provider_config->getURI()); + + if ($provider->isEnabled()) { + if ($provider->isAcceptingLivePayments()) { + $item->setStatusIcon('fa-check green'); + } else { + $item->setStatusIcon('fa-warning yellow'); + $item->addIcon('fa-exclamation-triangle', pht('Test Mode')); + } + + $item->addAttribute($provider->getConfigureProvidesDescription()); + } else { + $item->setDisabled(true); + $item->addAttribute( + phutil_tag('em', array(), pht('This payment provider is disabled.'))); + } + + $provider_list->addItem($item); + } + + $add_uri = urisprintf( + 'merchant/%d/providers/edit/', + $merchant->getID()); + $add_uri = $this->getApplicationURI($add_uri); + + $add_action = id(new PHUIButtonView()) + ->setTag('a') + ->setHref($add_uri) + ->setText(pht('Add Payment Provider')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon('fa-plus'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Providers')) + ->addActionLink($add_action); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($provider_list); + } + + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php new file mode 100644 index 0000000000..d5a8d4eef2 --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionListController.php @@ -0,0 +1,50 @@ +getViewer(); + $merchant = $this->getMerchant(); + + $engine = id(new PhortuneCartSearchEngine()) + ->setController($this) + ->setMerchant($merchant); + + $subscription_id = $request->getURIData('subscriptionID'); + if ($subscription_id) { + $subscription = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withIDs(array($subscription_id)) + ->executeOne(); + if (!$subscription) { + return new Aphront404Response(); + } + + $engine->setSubscription($subscription); + $this->subscription = $subscription; + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + if ($this->hasMerchant()) { + $merchant = $this->getMerchant(); + + $crumbs->addTextCrumb( + pht('Subscriptions'), + $merchant->getSubscriptionsURI()); + } + + return $crumbs; + } + + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php new file mode 100644 index 0000000000..c2fe0d429a --- /dev/null +++ b/src/applications/phortune/controller/merchant/PhortuneMerchantSubscriptionsController.php @@ -0,0 +1,68 @@ +getMerchant(); + $title = $merchant->getName(); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Subscriptions')) + ->setBorder(true); + + $header = $this->buildHeaderView(); + + $subscriptions = $this->buildSubscriptionsSection($merchant); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $subscriptions, + )); + + $navigation = $this->buildSideNavView('subscriptions'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->setNavigation($navigation) + ->appendChild($view); + } + + private function buildSubscriptionsSection(PhortuneMerchant $merchant) { + $viewer = $this->getViewer(); + + $subscriptions = id(new PhortuneSubscriptionQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->setLimit(25) + ->execute(); + + $subscriptions_uri = $merchant->getSubscriptionListURI(); + + $table = id(new PhortuneSubscriptionTableView()) + ->setUser($viewer) + ->setSubscriptions($subscriptions); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subscriptions')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list') + ->setHref($subscriptions_uri) + ->setText(pht('View All Subscriptions'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + +} diff --git a/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php b/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php deleted file mode 100644 index 10dee4b0ea..0000000000 --- a/src/applications/phortune/controller/merchant/PhortuneMerchantViewController.php +++ /dev/null @@ -1,324 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needProfileImage(true) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - - $this->setMerchant($merchant); - $crumbs = $this->buildApplicationCrumbs(); - $header = $this->buildHeaderView(); - - $title = pht( - 'Merchant %d %s', - $merchant->getID(), - $merchant->getName()); - - $providers = id(new PhortunePaymentProviderConfigQuery()) - ->setViewer($viewer) - ->withMerchantPHIDs(array($merchant->getPHID())) - ->execute(); - - $details = $this->buildDetailsView($merchant, $providers); - $curtain = $this->buildCurtainView($merchant); - - $provider_list = $this->buildProviderList( - $merchant, - $providers); - - $timeline = $this->buildTransactionTimeline( - $merchant, - new PhortuneMerchantTransactionQuery()); - $timeline->setShouldTerminate(true); - - $navigation = $this->buildSideNavView('overview'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $details, - $provider_list, - $timeline, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->setNavigation($navigation) - ->appendChild($view); - } - - private function buildDetailsView( - PhortuneMerchant $merchant, - array $providers) { - - $viewer = $this->getRequest()->getUser(); - - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($merchant); - - $status_view = new PHUIStatusListView(); - - $have_any = false; - $any_test = false; - foreach ($providers as $provider_config) { - $provider = $provider_config->buildProvider(); - if ($provider->isEnabled()) { - $have_any = true; - } - if (!$provider->isAcceptingLivePayments()) { - $any_test = true; - } - } - - if ($have_any) { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') - ->setTarget(pht('Accepts Payments')) - ->setNote(pht('This merchant can accept payments.'))); - - if ($any_test) { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') - ->setTarget(pht('Test Mode')) - ->setNote(pht('This merchant is accepting test payments.'))); - } else { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') - ->setTarget(pht('Live Mode')) - ->setNote(pht('This merchant is accepting live payments.'))); - } - } else if ($providers) { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_REJECT, 'red') - ->setTarget(pht('No Enabled Providers')) - ->setNote( - pht( - 'All of the payment providers for this merchant are '. - 'disabled.'))); - } else { - $status_view->addItem( - id(new PHUIStatusItemView()) - ->setIcon(PHUIStatusItemView::ICON_WARNING, 'yellow') - ->setTarget(pht('No Providers')) - ->setNote( - pht( - 'This merchant does not have any payment providers configured '. - 'yet, so it can not accept payments. Add a provider.'))); - } - - $view->addProperty(pht('Status'), $status_view); - - $invoice_from = $merchant->getInvoiceEmail(); - if (!$invoice_from) { - $invoice_from = pht('No email address set'); - $invoice_from = phutil_tag('em', array(), $invoice_from); - } - $view->addProperty(pht('Invoice From'), $invoice_from); - - $description = $merchant->getDescription(); - if (strlen($description)) { - $description = new PHUIRemarkupView($viewer, $description); - $view->addSectionHeader( - pht('Description'), - PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($description); - } - - $contact_info = $merchant->getContactInfo(); - if (strlen($contact_info)) { - $contact_info = new PHUIRemarkupView($viewer, $contact_info); - $view->addSectionHeader( - pht('Contact Info'), - PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($contact_info); - } - - $footer_info = $merchant->getInvoiceFooter(); - if (strlen($footer_info)) { - $footer_info = new PHUIRemarkupView($viewer, $footer_info); - $view->addSectionHeader( - pht('Invoice Footer'), - PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($footer_info); - } - - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Details')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($view); - } - - private function buildCurtainView(PhortuneMerchant $merchant) { - $viewer = $this->getRequest()->getUser(); - $id = $merchant->getID(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $merchant, - PhabricatorPolicyCapability::CAN_EDIT); - - $curtain = $this->newCurtainView($merchant); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Merchant')) - ->setIcon('fa-pencil') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setHref($this->getApplicationURI("merchant/edit/{$id}/"))); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Logo')) - ->setIcon('fa-camera') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setHref($this->getApplicationURI("merchant/picture/{$id}/"))); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('New Invoice')) - ->setIcon('fa-fax') - ->setHref($this->getApplicationURI("merchant/{$id}/invoice/new/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - - $member_phids = $merchant->getMemberPHIDs(); - $handles = $viewer->loadHandles($member_phids); - - $member_list = id(new PHUIObjectItemListView()) - ->setSimple(true); - - foreach ($member_phids as $member_phid) { - $image_uri = $handles[$member_phid]->getImageURI(); - $image_href = $handles[$member_phid]->getURI(); - $person = $handles[$member_phid]; - - $member = id(new PHUIObjectItemView()) - ->setImageURI($image_uri) - ->setHref($image_href) - ->setHeader($person->getFullName()); - - $member_list->addItem($member); - } - - $curtain->newPanel() - ->setHeaderText(pht('Managers')) - ->appendChild($member_list); - - return $curtain; - } - - private function buildProviderList( - PhortuneMerchant $merchant, - array $providers) { - - $viewer = $this->getRequest()->getUser(); - $id = $merchant->getID(); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $merchant, - PhabricatorPolicyCapability::CAN_EDIT); - - $provider_list = id(new PHUIObjectItemListView()) - ->setFlush(true) - ->setNoDataString(pht('This merchant has no payment providers.')); - - foreach ($providers as $provider_config) { - $provider = $provider_config->buildProvider(); - $provider_id = $provider_config->getID(); - - $item = id(new PHUIObjectItemView()) - ->setHeader($provider->getName()); - - if ($provider->isEnabled()) { - if ($provider->isAcceptingLivePayments()) { - $item->setStatusIcon('fa-check green'); - } else { - $item->setStatusIcon('fa-warning yellow'); - $item->addIcon('fa-exclamation-triangle', pht('Test Mode')); - } - - $item->addAttribute($provider->getConfigureProvidesDescription()); - } else { - // Don't show disabled providers to users who can't manage the merchant - // account. - if (!$can_edit) { - continue; - } - $item->setDisabled(true); - $item->addAttribute( - phutil_tag('em', array(), pht('This payment provider is disabled.'))); - } - - - if ($can_edit) { - $edit_uri = $this->getApplicationURI( - "/provider/edit/{$provider_id}/"); - $disable_uri = $this->getApplicationURI( - "/provider/disable/{$provider_id}/"); - - if ($provider->isEnabled()) { - $disable_icon = 'fa-times'; - $disable_name = pht('Disable'); - } else { - $disable_icon = 'fa-check'; - $disable_name = pht('Enable'); - } - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon($disable_icon) - ->setHref($disable_uri) - ->setName($disable_name) - ->setWorkflow(true)); - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-pencil') - ->setHref($edit_uri) - ->setName(pht('Edit'))); - } - - $provider_list->addItem($item); - } - - $add_action = id(new PHUIButtonView()) - ->setTag('a') - ->setHref($this->getApplicationURI('provider/edit/?merchantID='.$id)) - ->setText(pht('Add Payment Provider')) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setIcon('fa-plus'); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Payment Providers')) - ->addActionLink($add_action); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setObjectList($provider_list); - } - - -} diff --git a/src/applications/phortune/editor/PhortuneMerchantEditEngine.php b/src/applications/phortune/editor/PhortuneMerchantEditEngine.php index 04e07401c7..e09fba99ab 100644 --- a/src/applications/phortune/editor/PhortuneMerchantEditEngine.php +++ b/src/applications/phortune/editor/PhortuneMerchantEditEngine.php @@ -58,7 +58,7 @@ final class PhortuneMerchantEditEngine } protected function getObjectViewURI($object) { - return $object->getURI(); + return $object->getDetailsURI(); } public function isEngineConfigurable() { diff --git a/src/applications/phortune/editor/PhortuneMerchantEditor.php b/src/applications/phortune/editor/PhortuneMerchantEditor.php index 954570be3f..79fc7d534e 100644 --- a/src/applications/phortune/editor/PhortuneMerchantEditor.php +++ b/src/applications/phortune/editor/PhortuneMerchantEditor.php @@ -18,7 +18,6 @@ final class PhortuneMerchantEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDGE; return $types; diff --git a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php index 02d57e3b6a..8b8c731a84 100644 --- a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php @@ -26,7 +26,7 @@ final class PhortuneTestPaymentProvider extends PhortunePaymentProvider { } public function getConfigureInstructions() { - return pht('This providers does not require any special configuration.'); + return pht('This provider does not require any special configuration.'); } public function canRunConfigurationTest() { diff --git a/src/applications/phortune/storage/PhortuneMerchant.php b/src/applications/phortune/storage/PhortuneMerchant.php index 830ff8e1d5..6e0bf81e22 100644 --- a/src/applications/phortune/storage/PhortuneMerchant.php +++ b/src/applications/phortune/storage/PhortuneMerchant.php @@ -6,7 +6,6 @@ final class PhortuneMerchant extends PhortuneDAO PhabricatorPolicyInterface { protected $name; - protected $viewPolicy; protected $description; protected $contactInfo; protected $invoiceEmail; @@ -18,7 +17,6 @@ final class PhortuneMerchant extends PhortuneDAO public static function initializeNewMerchant(PhabricatorUser $actor) { return id(new PhortuneMerchant()) - ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->attachMemberPHIDs(array()) ->setContactInfo('') ->setInvoiceEmail('') @@ -74,6 +72,49 @@ final class PhortuneMerchant extends PhortuneDAO return pht('Merchant %d', $this->getID()); } + public function getDetailsURI() { + return urisprintf( + '/phortune/merchant/%d/details/', + $this->getID()); + } + + public function getOrdersURI() { + return urisprintf( + '/phortune/merchant/%d/orders/', + $this->getID()); + } + + public function getOrderListURI($path = '') { + return urisprintf( + '/phortune/merchant/%d/orders/list/%s', + $this->getID(), + $path); + } + + public function getSubscriptionsURI() { + return urisprintf( + '/phortune/merchant/%d/subscriptions/', + $this->getID()); + } + + public function getSubscriptionListURI($path = '') { + return urisprintf( + '/phortune/merchant/%d/subscriptions/list/%s', + $this->getID(), + $path); + } + + public function getManagersURI() { + return urisprintf( + '/phortune/merchant/%d/managers/', + $this->getID()); + } + + public function getPaymentProvidersURI() { + return urisprintf( + '/phortune/merchant/%d/providers/', + $this->getID()); + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -100,7 +141,7 @@ final class PhortuneMerchant extends PhortuneDAO public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: - return $this->getViewPolicy(); + return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php index 1e151b3fbb..985906519c 100644 --- a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php @@ -17,6 +17,7 @@ final class PhortunePaymentProviderConfig extends PhortuneDAO PhortuneMerchant $merchant) { return id(new PhortunePaymentProviderConfig()) ->setMerchantPHID($merchant->getPHID()) + ->attachMerchant($merchant) ->setIsEnabled(1); } @@ -75,6 +76,17 @@ final class PhortunePaymentProviderConfig extends PhortuneDAO ->setProviderConfig($this); } + public function getObjectName() { + return pht('Provider %d', $this->getID()); + } + + public function getURI() { + return urisprintf( + '/phortune/merchant/%d/providers/%d/', + $this->getMerchant()->getID(), + $this->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/view/PhortuneOrderTableView.php b/src/applications/phortune/view/PhortuneOrderTableView.php index 435f5151d0..b4b8866160 100644 --- a/src/applications/phortune/view/PhortuneOrderTableView.php +++ b/src/applications/phortune/view/PhortuneOrderTableView.php @@ -3,20 +3,10 @@ final class PhortuneOrderTableView extends AphrontView { private $carts; - private $handles; private $noDataString; private $isInvoices; private $isMerchantView; - public function setHandles(array $handles) { - $this->handles = $handles; - return $this; - } - - public function getHandles() { - return $this->handles; - } - public function setCarts(array $carts) { $this->carts = $carts; return $this; @@ -55,12 +45,22 @@ final class PhortuneOrderTableView extends AphrontView { public function render() { $carts = $this->getCarts(); - $handles = $this->getHandles(); $viewer = $this->getUser(); $is_invoices = $this->getIsInvoices(); $is_merchant = $this->getIsMerchantView(); + $phids = array(); + foreach ($carts as $cart) { + $phids[] = $cart->getPHID(); + foreach ($cart->getPurchases() as $purchase) { + $phids[] = $purchase->getPHID(); + } + $phids[] = $cart->getMerchantPHID(); + } + + $handles = $viewer->loadHandles($phids); + $rows = array(); $rowc = array(); foreach ($carts as $cart) { From b3f8045b873e042eece4c42f82cab278815ed287 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Aug 2019 21:28:14 -0700 Subject: [PATCH 111/122] Make minor flavor updates Summary: Refresh the 404 text since it hasn't been updated in a while, and swap the "Save Query" button back to grey since I never got used to blue. Test Plan: Hit 404 page, saved a query. Differential Revision: https://secure.phabricator.com/D20734 --- src/aphront/response/Aphront404Response.php | 13 ++++++++----- .../PhabricatorApplicationSearchController.php | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/aphront/response/Aphront404Response.php b/src/aphront/response/Aphront404Response.php index 1284cb62e6..ea98e9102b 100644 --- a/src/aphront/response/Aphront404Response.php +++ b/src/aphront/response/Aphront404Response.php @@ -8,16 +8,19 @@ final class Aphront404Response extends AphrontHTMLResponse { public function buildResponseString() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getViewer(); $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setViewer($viewer) ->setTitle(pht('404 Not Found')) - ->addCancelButton('/', pht('Focus')) + ->addCancelButton('/', pht('Return to Charted Waters')) ->appendParagraph( pht( - 'Do not dwell in the past, do not dream of the future, '. - 'concentrate the mind on the present moment.')); + 'You arrive at your destination, but there is nothing here.')) + ->appendParagraph( + pht( + 'Perhaps the real treasure was the friends you made '. + 'along the way.')); $view = id(new PhabricatorStandardPageView()) ->setTitle(pht('404 Not Found')) diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 81373ee190..98d125fd33 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -194,9 +194,10 @@ final class PhabricatorApplicationSearchController if ($run_query && !$named_query && $user->isLoggedIn()) { $save_button = id(new PHUIButtonView()) ->setTag('a') + ->setColor(PHUIButtonView::GREY) ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/') ->setText(pht('Save Query')) - ->setIcon('fa-floppy-o'); + ->setIcon('fa-bookmark'); $submit->addButton($save_button); } From a39a37fc0e3768244bf4a4e5a6c56b8809c42164 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 22 Aug 2019 21:28:37 -0700 Subject: [PATCH 112/122] Update the Phortune cart/invoice workflow for policy changes Summary: Depends on D20734. Ref T13366. This makes the cart/order flow work under the new policy scheme with no "grantAuthority()" calls. It prepares for a "Void Invoice" action, although the action doesn't actually do anything yet. Test Plan: With and without merchant authority, viewed and paid invoices and went through the other invoice interaction workflows. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20735 --- src/__phutil_library_map__.php | 6 +- .../PhabricatorPhortuneApplication.php | 2 + .../controller/PhortuneController.php | 62 ------ .../PhortuneAccountChargesController.php | 15 +- .../PhortuneAccountOrdersController.php | 2 +- .../PhortuneAccountProfileController.php | 4 +- .../cart/PhortuneCartAcceptController.php | 30 ++- .../cart/PhortuneCartCancelController.php | 31 ++- .../cart/PhortuneCartCheckoutController.php | 26 +-- .../cart/PhortuneCartController.php | 71 +++++++ .../cart/PhortuneCartUpdateController.php | 29 ++- .../cart/PhortuneCartViewController.php | 170 ++++++++-------- .../cart/PhortuneCartVoidController.php | 43 ++++ .../PhortuneSubscriptionEditController.php | 185 ------------------ .../PhortuneSubscriptionListController.php | 99 ---------- .../query/PhortuneCartSearchEngine.php | 2 +- .../query/PhortuneChargeSearchEngine.php | 19 +- .../PhortuneSubscriptionSearchEngine.php | 4 +- .../phortune/storage/PhortuneCart.php | 41 +++- .../phortune/view/PhortuneChargeTableView.php | 23 ++- 20 files changed, 300 insertions(+), 564 deletions(-) create mode 100644 src/applications/phortune/controller/cart/PhortuneCartVoidController.php delete mode 100644 src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php delete mode 100644 src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5fee952d0a..9e01eb700d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5279,6 +5279,7 @@ phutil_register_library_map(array( 'PhortuneCartTransactionQuery' => 'applications/phortune/query/PhortuneCartTransactionQuery.php', 'PhortuneCartUpdateController' => 'applications/phortune/controller/cart/PhortuneCartUpdateController.php', 'PhortuneCartViewController' => 'applications/phortune/controller/cart/PhortuneCartViewController.php', + 'PhortuneCartVoidController' => 'applications/phortune/controller/cart/PhortuneCartVoidController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php', 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', @@ -5372,10 +5373,8 @@ phutil_register_library_map(array( 'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php', 'PhortuneSubscriptionAutopayTransaction' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php', 'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php', - 'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php', 'PhortuneSubscriptionEditor' => 'applications/phortune/editor/PhortuneSubscriptionEditor.php', 'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php', - 'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php', 'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php', 'PhortuneSubscriptionPolicyCodex' => 'applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php', 'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php', @@ -11861,6 +11860,7 @@ phutil_register_library_map(array( 'PhortuneCartTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneCartUpdateController' => 'PhortuneCartController', 'PhortuneCartViewController' => 'PhortuneCartController', + 'PhortuneCartVoidController' => 'PhortuneCartController', 'PhortuneCharge' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', @@ -11984,10 +11984,8 @@ phutil_register_library_map(array( ), 'PhortuneSubscriptionAutopayTransaction' => 'PhortuneSubscriptionTransactionType', 'PhortuneSubscriptionCart' => 'PhortuneCartImplementation', - 'PhortuneSubscriptionEditController' => 'PhortuneController', 'PhortuneSubscriptionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneSubscriptionImplementation' => 'Phobject', - 'PhortuneSubscriptionListController' => 'PhortuneController', 'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType', 'PhortuneSubscriptionPolicyCodex' => 'PhabricatorPolicyCodex', 'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 15eb500fc3..18a5d45f69 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -43,6 +43,8 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'checkout/' => 'PhortuneCartCheckoutController', '(?Pprint)/' => 'PhortuneCartViewController', '(?Pcancel|refund)/' => 'PhortuneCartCancelController', + 'accept/' => 'PhortuneCartAcceptController', + 'void/' => 'PhortuneCartVoidController', 'update/' => 'PhortuneCartUpdateController', ), 'account/' => array( diff --git a/src/applications/phortune/controller/PhortuneController.php b/src/applications/phortune/controller/PhortuneController.php index 655dcee4e1..37896cc941 100644 --- a/src/applications/phortune/controller/PhortuneController.php +++ b/src/applications/phortune/controller/PhortuneController.php @@ -2,42 +2,6 @@ abstract class PhortuneController extends PhabricatorController { - protected function addAccountCrumb( - $crumbs, - PhortuneAccount $account, - $link = true) { - - $name = $account->getName(); - $href = null; - - if ($link) { - $href = $this->getApplicationURI($account->getID().'/'); - $crumbs->addTextCrumb($name, $href); - } else { - $crumbs->addTextCrumb($name); - } - } - - protected function addMerchantCrumb( - $crumbs, - PhortuneMerchant $merchant, - $link = true) { - - $name = $merchant->getName(); - $href = null; - - $crumbs->addTextCrumb( - pht('Merchants'), - $this->getApplicationURI('merchant/')); - - if ($link) { - $href = $this->getApplicationURI('merchant/'.$merchant->getID().'/'); - $crumbs->addTextCrumb($name, $href); - } else { - $crumbs->addTextCrumb($name); - } - } - private function loadEnabledProvidersForMerchant(PhortuneMerchant $merchant) { $viewer = $this->getRequest()->getUser(); @@ -84,30 +48,4 @@ abstract class PhortuneController extends PhabricatorController { return $providers; } - protected function loadMerchantAuthority() { - $request = $this->getRequest(); - $viewer = $this->getViewer(); - - $is_merchant = (bool)$request->getURIData('merchantID'); - if (!$is_merchant) { - return null; - } - - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('merchantID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return null; - } - - $viewer->grantAuthority($merchant); - return $merchant; - } - } diff --git a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php index db85698be7..ea6105504f 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountChargesController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountChargesController.php @@ -12,7 +12,7 @@ final class PhortuneAccountChargesController $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb(pht('Order History')) + ->addTextCrumb(pht('Orders')) ->setBorder(true); $header = $this->buildHeaderView(); @@ -46,22 +46,11 @@ final class PhortuneAccountChargesController ->setLimit(100) ->execute(); - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - - $handles = $this->loadViewerHandles($phids); - $charges_uri = $account->getChargeListURI(); $table = id(new PhortuneChargeTableView()) ->setUser($viewer) - ->setCharges($charges) - ->setHandles($handles); + ->setCharges($charges); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Charges')) diff --git a/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php index 902d2032ff..6ca57453ed 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountOrdersController.php @@ -12,7 +12,7 @@ final class PhortuneAccountOrdersController $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb(pht('Order History')) + ->addTextCrumb(pht('Orders')) ->setBorder(true); $header = $this->buildHeaderView(); diff --git a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php index 33361c1f66..212dfcd5a7 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountProfileController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountProfileController.php @@ -66,13 +66,13 @@ abstract class PhortuneAccountProfileController $nav->addFilter( 'orders', - pht('Order History'), + pht('Orders'), $account->getOrdersURI(), 'fa-shopping-bag'); $nav->addFilter( 'charges', - pht('Charge History'), + pht('Charges'), $account->getChargesURI(), 'fa-calculator'); diff --git a/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php b/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php index cb53e66f50..79969b999d 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartAcceptController.php @@ -3,27 +3,19 @@ final class PhortuneCartAcceptController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return true; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); + $cart = $this->getCart(); - // You must control the merchant to accept orders. - $authority = $this->loadMerchantAuthority(); - if (!$authority) { - return new Aphront404Response(); - } - - $cart = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->withMerchantPHIDs(array($authority->getPHID())) - ->needPurchases(true) - ->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } - - $cancel_uri = $cart->getDetailURI($authority); + $cancel_uri = $cart->getDetailURI(); if ($cart->getStatus() !== PhortuneCart::STATUS_REVIEW) { return $this->newDialog() diff --git a/src/applications/phortune/controller/cart/PhortuneCartCancelController.php b/src/applications/phortune/controller/cart/PhortuneCartCancelController.php index c4a26c0d00..bad476ea1c 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCancelController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCancelController.php @@ -3,26 +3,21 @@ final class PhortuneCartCancelController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); - $authority = $this->loadMerchantAuthority(); - - $cart_query = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true); - - if ($authority) { - $cart_query->withMerchantPHIDs(array($authority->getPHID())); - } - - $cart = $cart_query->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } + $cart = $this->getCart(); + $authority = $this->getMerchantAuthority(); switch ($action) { case 'cancel': @@ -45,7 +40,7 @@ final class PhortuneCartCancelController return new Aphront404Response(); } - $cancel_uri = $cart->getDetailURI($authority); + $cancel_uri = $cart->getDetailURI(); $merchant = $cart->getMerchant(); try { @@ -60,7 +55,7 @@ final class PhortuneCartCancelController return $this->newDialog() ->setTitle($title) ->appendChild($ex->getMessage()) - ->addCancelButton($cancel_uri); + ->addCancelButton($cancel_uri, pht('Rats')); } $charges = id(new PhortuneChargeQuery()) diff --git a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php index 874ecf63aa..4b59afef75 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php @@ -3,18 +3,17 @@ final class PhortuneCartCheckoutController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { - $viewer = $request->getViewer(); - $id = $request->getURIData('id'); + protected function shouldRequireAccountAuthority() { + return true; + } - $cart = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true) - ->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { + $viewer = $request->getViewer(); + $cart = $this->getCart(); $cancel_uri = $cart->getCancelURI(); $merchant = $cart->getMerchant(); @@ -139,7 +138,10 @@ final class PhortuneCartCheckoutController 'cartID' => $cart->getID(), ); - $payment_method_uri = $this->getApplicationURI("{$account_id}/card/new/"); + $payment_method_uri = urisprintf( + 'account/%d/methods/new/', + $account->getID()); + $payment_method_uri = $this->getApplicationURI($payment_method_uri); $payment_method_uri = new PhutilURI($payment_method_uri, $params); $form = id(new AphrontFormView()) diff --git a/src/applications/phortune/controller/cart/PhortuneCartController.php b/src/applications/phortune/controller/cart/PhortuneCartController.php index b8f926d3b2..2f5f55014e 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartController.php @@ -3,6 +3,77 @@ abstract class PhortuneCartController extends PhortuneController { + private $cart; + private $merchantAuthority; + + abstract protected function shouldRequireAccountAuthority(); + abstract protected function shouldRequireMerchantAuthority(); + abstract protected function handleCartRequest(AphrontRequest $request); + + final public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + if ($this->shouldRequireAccountAuthority()) { + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } else { + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + $cart = id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->needPurchases(true) + ->requireCapabilities($capabilities) + ->executeOne(); + if (!$cart) { + return new Aphront404Response(); + } + + if ($this->shouldRequireMerchantAuthority()) { + PhabricatorPolicyFilter::requireCapability( + $viewer, + $cart->getMerchant(), + PhabricatorPolicyCapability::CAN_EDIT); + } + + $this->cart = $cart; + + $can_edit = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($cart->getMerchantPHID())); + if ($can_edit) { + $this->merchantAuthority = $cart->getMerchant(); + } else { + $this->merchantAuthority = null; + } + + return $this->handleCartRequest($request); + } + + final protected function getCart() { + return $this->cart; + } + + final protected function getMerchantAuthority() { + return $this->merchantAuthority; + } + + final protected function hasMerchantAuthority() { + return (bool)$this->merchantAuthority; + } + + final protected function hasAccountAuthority() { + return (bool)PhabricatorPolicyFilter::hasCapability( + $this->getViewer(), + $this->getCart(), + PhabricatorPolicyCapability::CAN_EDIT); + } + protected function buildCartContentTable(PhortuneCart $cart) { $rows = array(); diff --git a/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php b/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php index 3d49611d2d..64b2ab3cfa 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartUpdateController.php @@ -3,25 +3,20 @@ final class PhortuneCartUpdateController extends PhortuneCartController { - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); - $authority = $this->loadMerchantAuthority(); - - $cart_query = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true); - - if ($authority) { - $cart_query->withMerchantPHIDs(array($authority->getPHID())); - } - - $cart = $cart_query->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } + $cart = $this->getCart(); + $authority = $this->getMerchantAuthority(); $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) @@ -60,7 +55,7 @@ final class PhortuneCartUpdateController } return id(new AphrontRedirectResponse()) - ->setURI($cart->getDetailURI($authority)); + ->setURI($cart->getDetailURI()); } } diff --git a/src/applications/phortune/controller/cart/PhortuneCartViewController.php b/src/applications/phortune/controller/cart/PhortuneCartViewController.php index 8108c83b39..d728a202c9 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartViewController.php @@ -5,62 +5,33 @@ final class PhortuneCartViewController private $action = null; - public function handleRequest(AphrontRequest $request) { + protected function shouldRequireAccountAuthority() { + return false; + } + + protected function shouldRequireMerchantAuthority() { + return false; + } + + protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); + $cart = $this->getCart(); + $authority = $this->getMerchantAuthority(); + $can_edit = $this->hasAccountAuthority(); + $this->action = $request->getURIData('action'); - $authority = $this->loadMerchantAuthority(); - require_celerity_resource('phortune-css'); - - $query = id(new PhortuneCartQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needPurchases(true); - - if ($authority) { - $query->withMerchantPHIDs(array($authority->getPHID())); - } - - $cart = $query->executeOne(); - if (!$cart) { - return new Aphront404Response(); - } - $cart_table = $this->buildCartContentTable($cart); - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $cart, - PhabricatorPolicyCapability::CAN_EDIT); - $errors = array(); $error_view = null; $resume_uri = null; switch ($cart->getStatus()) { case PhortuneCart::STATUS_READY: - if ($authority && $cart->getIsInvoice()) { - // We arrived here by following the ad-hoc invoice workflow, and - // are acting with merchant authority. - - $checkout_uri = PhabricatorEnv::getURI($cart->getCheckoutURI()); - - $invoice_message = array( - pht( - 'Manual invoices do not automatically notify recipients yet. '. - 'Send the payer this checkout link:'), - ' ', - phutil_tag( - 'a', - array( - 'href' => $checkout_uri, - ), - $checkout_uri), - ); - + if ($cart->getIsInvoice()) { $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->setErrors(array($invoice_message)); + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->appendChild(pht('This invoice is ready for payment.')); } break; case PhortuneCart::STATUS_PURCHASING: @@ -133,7 +104,7 @@ final class PhortuneCartViewController $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($cart->getName()) - ->setHeaderIcon('fa-shopping-cart'); + ->setHeaderIcon('fa-shopping-bag'); if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) { $done_uri = $cart->getDoneURI(); @@ -160,18 +131,8 @@ final class PhortuneCartViewController ->needCarts(true) ->execute(); - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - $handles = $this->loadViewerHandles($phids); - $charges_table = id(new PhortuneChargeTableView()) ->setUser($viewer) - ->setHandles($handles) ->setCharges($charges) ->setShowOrder(false); @@ -182,14 +143,13 @@ final class PhortuneCartViewController $account = $cart->getAccount(); - $crumbs = $this->buildApplicationCrumbs(); - if ($authority) { - $this->addMerchantCrumb($crumbs, $authority); - } else { - $this->addAccountCrumb($crumbs, $cart->getAccount()); - } - $crumbs->addTextCrumb(pht('Cart %d', $cart->getID())); - $crumbs->setBorder(true); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($account->getName(), $account->getURI()) + ->addTextCrumb(pht('Orders'), $account->getOrdersURI()) + ->addTextCrumb(pht('Cart %d', $cart->getID())) + ->setBorder(true); + + require_celerity_resource('phortune-css'); if (!$this->action) { $class = 'phortune-cart-page'; @@ -267,6 +227,7 @@ final class PhortuneCartViewController if ($crumbs) { $page->setCrumbs($crumbs); } + return $page; } @@ -318,20 +279,31 @@ final class PhortuneCartViewController $viewer = $this->getViewer(); $id = $cart->getID(); $curtain = $this->newCurtainView($cart); + $status = $cart->getStatus(); + + $is_ready = ($status === PhortuneCart::STATUS_READY); $can_cancel = ($can_edit && $cart->canCancelOrder()); + $can_checkout = ($can_edit && $is_ready); + $can_accept = ($status === PhortuneCart::STATUS_REVIEW); + $can_refund = ($authority && $cart->canRefundOrder()); + $can_void = ($authority && $cart->canVoidOrder()); - if ($authority) { - $prefix = 'merchant/'.$authority->getID().'/'; - } else { - $prefix = ''; - } + $cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/"); + $refund_uri = $this->getApplicationURI("cart/{$id}/refund/"); + $update_uri = $this->getApplicationURI("cart/{$id}/update/"); + $accept_uri = $this->getApplicationURI("cart/{$id}/accept/"); + $print_uri = $this->getApplicationURI("cart/{$id}/print/"); + $checkout_uri = $cart->getCheckoutURI(); + $void_uri = $this->getApplicationURI("cart/{$id}/void/"); - $cancel_uri = $this->getApplicationURI("{$prefix}cart/{$id}/cancel/"); - $refund_uri = $this->getApplicationURI("{$prefix}cart/{$id}/refund/"); - $update_uri = $this->getApplicationURI("{$prefix}cart/{$id}/update/"); - $accept_uri = $this->getApplicationURI("{$prefix}cart/{$id}/accept/"); - $print_uri = $this->getApplicationURI("{$prefix}cart/{$id}/print/"); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Pay Now')) + ->setIcon('fa-credit-card') + ->setDisabled(!$can_checkout) + ->setWorkflow(!$can_checkout) + ->setHref($checkout_uri)); $curtain->addAction( id(new PhabricatorActionView()) @@ -341,24 +313,6 @@ final class PhortuneCartViewController ->setWorkflow(true) ->setHref($cancel_uri)); - if ($authority) { - if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) { - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Accept Order')) - ->setIcon('fa-check') - ->setWorkflow(true) - ->setHref($accept_uri)); - } - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Refund Order')) - ->setIcon('fa-reply') - ->setWorkflow(true) - ->setHref($refund_uri)); - } - $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Update Status')) @@ -369,7 +323,7 @@ final class PhortuneCartViewController $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Continue Checkout')) - ->setIcon('fa-shopping-cart') + ->setIcon('fa-shopping-bag') ->setHref($resume_uri)); } @@ -380,6 +334,36 @@ final class PhortuneCartViewController ->setOpenInNewWindow(true) ->setIcon('fa-print')); + if ($authority) { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Accept Order')) + ->setIcon('fa-check') + ->setWorkflow(true) + ->setDisabled(!$can_accept) + ->setHref($accept_uri)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Refund Order')) + ->setIcon('fa-reply') + ->setWorkflow(true) + ->setDisabled(!$can_refund) + ->setHref($refund_uri)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Void Invoice')) + ->setIcon('fa-times') + ->setWorkflow(true) + ->setDisabled(!$can_void) + ->setHref($void_uri)); + } + return $curtain; } diff --git a/src/applications/phortune/controller/cart/PhortuneCartVoidController.php b/src/applications/phortune/controller/cart/PhortuneCartVoidController.php new file mode 100644 index 0000000000..184775b10d --- /dev/null +++ b/src/applications/phortune/controller/cart/PhortuneCartVoidController.php @@ -0,0 +1,43 @@ +getViewer(); + $cart = $this->getCart(); + + $cancel_uri = $cart->getDetailURI(); + + try { + $title = pht('Unable to Void Invoice'); + $cart->assertCanVoidOrder(); + } catch (Exception $ex) { + return $this->newDialog() + ->setTitle($title) + ->appendChild($ex->getMessage()) + ->addCancelButton($cancel_uri); + } + + if ($request->isFormPost()) { + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + return $this->newDialog() + ->setTitle(pht('Void Invoice?')) + ->appendParagraph( + pht( + 'Really void this invoice? The customer will no longer be asked '. + 'to submit payment for it.')) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Void Invoice')); + } +} diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php deleted file mode 100644 index 04367a88a0..0000000000 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php +++ /dev/null @@ -1,185 +0,0 @@ -getViewer(); - $added = $request->getBool('added'); - - $subscription = id(new PhortuneSubscriptionQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getURIData('id'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$subscription) { - return new Aphront404Response(); - } - - id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $subscription->getURI()); - $merchant = $subscription->getMerchant(); - $account = $subscription->getAccount(); - - $title = pht('Subscription: %s', $subscription->getSubscriptionName()); - - $header = id(new PHUIHeaderView()) - ->setHeader($subscription->getSubscriptionName()); - - $view_uri = $subscription->getURI(); - - $valid_methods = id(new PhortunePaymentMethodQuery()) - ->setViewer($viewer) - ->withAccountPHIDs(array($account->getPHID())) - ->withStatuses( - array( - PhortunePaymentMethod::STATUS_ACTIVE, - )) - ->withMerchantPHIDs(array($merchant->getPHID())) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->execute(); - $valid_methods = mpull($valid_methods, null, 'getPHID'); - - $current_phid = $subscription->getDefaultPaymentMethodPHID(); - - $e_method = null; - if ($current_phid && empty($valid_methods[$current_phid])) { - $e_method = pht('Needs Update'); - } - - $errors = array(); - if ($request->isFormPost()) { - - $default_method_phid = $request->getStr('defaultPaymentMethodPHID'); - if (!$default_method_phid) { - $default_method_phid = null; - $e_method = null; - } else if (empty($valid_methods[$default_method_phid])) { - $e_method = pht('Invalid'); - if ($default_method_phid == $current_phid) { - $errors[] = pht( - 'This subscription is configured to autopay with a payment method '. - 'that has been deleted. Choose a valid payment method or disable '. - 'autopay.'); - } else { - $errors[] = pht('You must select a valid default payment method.'); - } - } - - // TODO: We should use transactions here, and move the validation logic - // inside the Editor. - - if (!$errors) { - $subscription->setDefaultPaymentMethodPHID($default_method_phid); - $subscription->save(); - - return id(new AphrontRedirectResponse()) - ->setURI($view_uri); - } - } - - // Add the option to disable autopay. - $disable_options = array( - '' => pht('(Disable Autopay)'), - ); - - // Don't require the user to make a valid selection if the current method - // has become invalid. - if ($current_phid && empty($valid_methods[$current_phid])) { - $current_options = array( - $current_phid => pht(''), - ); - } else { - $current_options = array(); - } - - // Add any available options. - $valid_options = mpull($valid_methods, 'getFullDisplayName', 'getPHID'); - - $options = $disable_options + $current_options + $valid_options; - - $crumbs = $this->buildApplicationCrumbs(); - $this->addAccountCrumb($crumbs, $account); - $crumbs->addTextCrumb( - pht('Subscription %d', $subscription->getID()), - $view_uri); - $crumbs->addTextCrumb(pht('Edit')); - $crumbs->setBorder(true); - - - $uri = $this->getApplicationURI($account->getID().'/card/new/'); - $uri = new PhutilURI($uri); - $uri->replaceQueryParam('merchantID', $merchant->getID()); - $uri->replaceQueryParam('subscriptionID', $subscription->getID()); - - $add_method_button = phutil_tag( - 'a', - array( - 'href' => $uri, - 'class' => 'button button-grey', - ), - pht('Add Payment Method...')); - - $radio = id(new AphrontFormRadioButtonControl()) - ->setName('defaultPaymentMethodPHID') - ->setLabel(pht('Autopay With')) - ->setValue($current_phid) - ->setError($e_method); - - foreach ($options as $key => $value) { - $radio->addButton($key, $value, null); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild($radio) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setValue($add_method_button)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Save Changes')) - ->addCancelButton($view_uri)); - - $box = id(new PHUIObjectBoxView()) - ->setUser($viewer) - ->setHeaderText(pht('Subscription')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setFormErrors($errors) - ->appendChild($form); - - if ($added) { - $info_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_SUCCESS) - ->appendChild(pht('Payment method has been successfully added.')); - $box->setInfoView($info_view); - } - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Edit %s', $subscription->getSubscriptionName())) - ->setHeaderIcon('fa-pencil'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - - } - - -} diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php deleted file mode 100644 index 469960f4bc..0000000000 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionListController.php +++ /dev/null @@ -1,99 +0,0 @@ -getViewer(); - $querykey = $request->getURIData('queryKey'); - $merchant_id = $request->getURIData('merchantID'); - $account_id = $request->getURIData('accountID'); - - $engine = new PhortuneSubscriptionSearchEngine(); - - if ($merchant_id) { - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withIDs(array($merchant_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$merchant) { - return new Aphront404Response(); - } - $this->merchant = $merchant; - $viewer->grantAuthority($merchant); - $engine->setMerchant($merchant); - } else if ($account_id) { - $account = id(new PhortuneAccountQuery()) - ->setViewer($viewer) - ->withIDs(array($account_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$account) { - return new Aphront404Response(); - } - $this->account = $account; - $engine->setAccount($account); - } else { - return new Aphront404Response(); - } - - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine($engine) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView() { - $viewer = $this->getViewer(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhortuneSubscriptionSearchEngine()) - ->setViewer($viewer) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $merchant = $this->merchant; - if ($merchant) { - $id = $merchant->getID(); - $this->addMerchantCrumb($crumbs, $merchant); - $crumbs->addTextCrumb( - pht('Subscriptions'), - $this->getApplicationURI("merchant/subscriptions/{$id}/")); - } - - $account = $this->account; - if ($account) { - $id = $account->getID(); - $this->addAccountCrumb($crumbs, $account); - $crumbs->addTextCrumb( - pht('Subscriptions'), - $this->getApplicationURI("{$id}/subscription/")); - } - - return $crumbs; - } - -} diff --git a/src/applications/phortune/query/PhortuneCartSearchEngine.php b/src/applications/phortune/query/PhortuneCartSearchEngine.php index da719c17c0..a4a0f2848d 100644 --- a/src/applications/phortune/query/PhortuneCartSearchEngine.php +++ b/src/applications/phortune/query/PhortuneCartSearchEngine.php @@ -105,7 +105,7 @@ final class PhortuneCartSearchEngine $merchant = $this->getMerchant(); $account = $this->getAccount(); if ($merchant) { - return '/phortune/merchant/orders/'.$merchant->getID().'/'.$path; + return $merchant->getOrderListURI($path); } else if ($account) { return $account->getOrderListURI($path); } else { diff --git a/src/applications/phortune/query/PhortuneChargeSearchEngine.php b/src/applications/phortune/query/PhortuneChargeSearchEngine.php index 45316118af..e1fb5a47ba 100644 --- a/src/applications/phortune/query/PhortuneChargeSearchEngine.php +++ b/src/applications/phortune/query/PhortuneChargeSearchEngine.php @@ -62,7 +62,7 @@ final class PhortuneChargeSearchEngine protected function getURI($path) { $account = $this->getAccount(); if ($account) { - return '/phortune/'.$account->getID().'/charge/'; + return $account->getChargeListURI($path); } else { return '/phortune/charge/'.$path; } @@ -89,20 +89,6 @@ final class PhortuneChargeSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $charges, - PhabricatorSavedQuery $query) { - - $phids = array(); - foreach ($charges as $charge) { - $phids[] = $charge->getProviderPHID(); - $phids[] = $charge->getCartPHID(); - $phids[] = $charge->getMerchantPHID(); - $phids[] = $charge->getPaymentMethodPHID(); - } - - return $phids; - } protected function renderResultList( array $charges, @@ -114,8 +100,7 @@ final class PhortuneChargeSearchEngine $table = id(new PhortuneChargeTableView()) ->setUser($viewer) - ->setCharges($charges) - ->setHandles($handles); + ->setCharges($charges); $result = new PhabricatorApplicationSearchResultView(); $result->setTable($table); diff --git a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php index 3f4bfb4e84..0d2e720aa7 100644 --- a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php +++ b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php @@ -96,9 +96,9 @@ final class PhortuneSubscriptionSearchEngine $merchant = $this->getMerchant(); $account = $this->getAccount(); if ($merchant) { - return '/phortune/merchant/'.$merchant->getID().'/subscription/'.$path; + return $merchant->getSubscriptionListURI($path); } else if ($account) { - return '/phortune/'.$account->getID().'/subscription/'; + return $account->getSubscriptionListURI($path); } else { return '/phortune/subscription/'.$path; } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index f551cbb5fb..624a13d6b0 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -471,13 +471,10 @@ final class PhortuneCart extends PhortuneDAO return $this->getImplementation()->getDescription($this); } - public function getDetailURI(PhortuneMerchant $authority = null) { - if ($authority) { - $prefix = 'merchant/'.$authority->getID().'/'; - } else { - $prefix = ''; - } - return '/phortune/'.$prefix.'cart/'.$this->getID().'/'; + public function getDetailURI() { + return urisprintf( + '/phortune/cart/%d/', + $this->getID()); } public function getCheckoutURI() { @@ -502,6 +499,15 @@ final class PhortuneCart extends PhortuneDAO } } + public function canVoidOrder() { + try { + $this->assertCanVoidOrder(); + return true; + } catch (Exception $ex) { + return false; + } + } + public function assertCanCancelOrder() { switch ($this->getStatus()) { case self::STATUS_BUILDING: @@ -534,6 +540,27 @@ final class PhortuneCart extends PhortuneDAO return $this->getImplementation()->assertCanRefundOrder($this); } + public function assertCanVoidOrder() { + if (!$this->getIsInvoice()) { + throw new Exception( + pht( + 'This order can not be voided because it is not an invoice.')); + } + + switch ($this->getStatus()) { + case self::STATUS_READY: + break; + default: + throw new Exception( + pht( + 'This order can not be voided because it is not ready for '. + 'payment.')); + } + + return null; + } + + protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/src/applications/phortune/view/PhortuneChargeTableView.php b/src/applications/phortune/view/PhortuneChargeTableView.php index 663c470a81..1e8dd2cc9a 100644 --- a/src/applications/phortune/view/PhortuneChargeTableView.php +++ b/src/applications/phortune/view/PhortuneChargeTableView.php @@ -3,7 +3,6 @@ final class PhortuneChargeTableView extends AphrontView { private $charges; - private $handles; private $showOrder; public function setShowOrder($show_order) { @@ -15,15 +14,6 @@ final class PhortuneChargeTableView extends AphrontView { return $this->showOrder; } - public function setHandles(array $handles) { - $this->handles = $handles; - return $this; - } - - public function getHandles() { - return $this->handles; - } - public function setCharges(array $charges) { $this->charges = $charges; return $this; @@ -35,8 +25,17 @@ final class PhortuneChargeTableView extends AphrontView { public function render() { $charges = $this->getCharges(); - $handles = $this->getHandles(); - $viewer = $this->getUser(); + $viewer = $this->getViewer(); + + $phids = array(); + foreach ($charges as $charge) { + $phids[] = $charge->getCartPHID(); + $phids[] = $charge->getProviderPHID(); + $phids[] = $charge->getPaymentMethodPHID(); + $phids[] = $charge->getMerchantPHID(); + } + + $handles = $viewer->loadHandles($phids); $rows = array(); foreach ($charges as $charge) { From 8f6a1ab015949d88977de7d260fd9dca99e34333 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 23 Aug 2019 08:07:41 -0700 Subject: [PATCH 113/122] Roughly support external/email user views of Phortune recipts and invoices Summary: Ref T13366. This gives each account email address an "external portal" section so they can access invoices and receipts without an account. Test Plan: Viewed portal as user with authority and in an incognito window. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20737 --- src/__phutil_library_map__.php | 4 + .../PhabricatorPhortuneApplication.php | 3 + .../PhortuneAccountEmailViewController.php | 14 ++ .../external/PhortuneExternalController.php | 147 ++++++++++++++++++ .../PhortuneExternalOverviewController.php | 91 +++++++++++ .../query/PhortuneAccountEmailQuery.php | 13 ++ .../phortune/storage/PhortuneAccountEmail.php | 7 + .../phortune/view/PhortuneOrderTableView.php | 24 +-- 8 files changed, 293 insertions(+), 10 deletions(-) create mode 100644 src/applications/phortune/controller/external/PhortuneExternalController.php create mode 100644 src/applications/phortune/controller/external/PhortuneExternalOverviewController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9e01eb700d..ec3ef2231b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5294,6 +5294,8 @@ phutil_register_library_map(array( 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 'PhortuneDisplayException' => 'applications/phortune/exception/PhortuneDisplayException.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', + 'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php', + 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', @@ -11879,6 +11881,8 @@ phutil_register_library_map(array( 'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneDisplayException' => 'Exception', 'PhortuneErrCode' => 'PhortuneConstants', + 'PhortuneExternalController' => 'PhortuneController', + 'PhortuneExternalOverviewController' => 'PhortuneExternalController', 'PhortuneInvoiceView' => 'AphrontTagView', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 18a5d45f69..43594c9653 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -104,6 +104,9 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '(?P\d+)/(?P[^/]+)/' => 'PhortuneProviderActionController', ), + 'external/(?P[^/]+)/(?P[^/]+)/' => array( + '' => 'PhortuneExternalOverviewController', + ), 'merchant/' => array( $this->getQueryRoutePattern() => 'PhortuneMerchantListController', diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php index 14c2b842f9..97d90946f5 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -67,6 +67,12 @@ final class PhortuneAccountEmailViewController $account->getID(), $address->getID())); + if ($can_edit) { + $external_uri = $address->getExternalURI(); + } else { + $external_uri = null; + } + $curtain = $this->newCurtainView($account); $curtain->addAction( @@ -77,6 +83,14 @@ final class PhortuneAccountEmailViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Show External View')) + ->setIcon('fa-eye') + ->setHref($external_uri) + ->setDisabled(!$can_edit) + ->setOpenInNewWindow(true)); + return $curtain; } diff --git a/src/applications/phortune/controller/external/PhortuneExternalController.php b/src/applications/phortune/controller/external/PhortuneExternalController.php new file mode 100644 index 0000000000..72ea02a955 --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalController.php @@ -0,0 +1,147 @@ +email; + } + + final protected function getAccountEmail() { + return $this->email; + } + + final protected function getExternalViewer() { + return PhabricatorUser::getOmnipotentUser(); + } + + final public function handleRequest(AphrontRequest $request) { + $address_key = $request->getURIData('addressKey'); + $access_key = $request->getURIData('accessKey'); + + $viewer = $this->getViewer(); + $xviewer = $this->getExternalViewer(); + + $email = id(new PhortuneAccountEmailQuery()) + ->setViewer($xviewer) + ->withAddressKeys(array($address_key)) + ->executeOne(); + if (!$email) { + return new Aphront404Response(); + } + + $account = $email->getAccount(); + + $can_see = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_EDIT); + + $email_display = phutil_tag('strong', array(), $email->getAddress()); + $user_display = phutil_tag('strong', array(), $viewer->getUsername()); + + $actual_key = $email->getAccessKey(); + if (!phutil_hashes_are_identical($access_key, $actual_key)) { + $dialog = $this->newDialog() + ->setTitle(pht('Email Access Link Out of Date')) + ->appendParagraph( + pht( + 'You are trying to access this payment account as: %s', + $email_display)) + ->appendParagraph( + pht( + 'The access link you have followed is out of date and no longer '. + 'works.')); + + if ($can_see) { + $dialog->appendParagraph( + pht( + 'You are currently logged in as a user (%s) who has '. + 'permission to manage the payment account, so you can '. + 'continue to the updated link.', + $user_display)); + + $dialog->addCancelButton( + $email->getExternalURI(), + pht('Continue to Updated Link')); + } else { + $dialog->appendParagraph( + pht( + 'To access information about this payment account, follow '. + 'a more recent link or ask a user with access to give you '. + 'an updated link.')); + } + + return $dialog; + } + + // TODO: Test that status is good. + + $this->email = $email; + + return $this->handleExternalRequest($request); + } + + final protected function newExternalCrumbs() { + $viewer = $this->getViewer(); + + $crumbs = new PHUICrumbsView(); + + if ($this->hasAccountEmail()) { + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $crumb_name = pht( + 'Payment Account: %s', + $account->getName()); + + $crumb = id(new PHUICrumbView()) + ->setIcon('fa-diamond') + ->setName($crumb_name); + + $can_see = PhabricatorPolicyFilter::hasCapability( + $viewer, + $account, + PhabricatorPolicyCapability::CAN_VIEW); + if ($can_see) { + $crumb->setHref($account->getURI()); + } + + $crumbs + ->addCrumb($crumb) + ->addTextCrumb(pht('Viewing As "%s"', $email->getAddress())); + } else { + $crumb = id(new PHUICrumbView()) + ->setIcon('fa-diamond') + ->setText(pht('External Account View')); + + $crumbs->addCrumb($crumb); + } + + return $crumbs; + } + + final protected function newExternalView() { + $email = $this->getAccountEmail(); + + $messages = array(); + $messages[] = pht( + 'You are viewing this payment account as: %s', + phutil_tag('strong', array(), $email->getAddress())); + $messages[] = pht( + 'Anyone who has a link to this page can view order history for '. + 'this payment account.'); + + return id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors($messages); + } +} diff --git a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php new file mode 100644 index 0000000000..bcd68e2f4a --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php @@ -0,0 +1,91 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $crumbs = $this->newExternalCrumbs() + ->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices and Receipts: %s', $account->getName())); + + $external_view = $this->newExternalView(); + $invoices_view = $this->newInvoicesView(); + $receipts_view = $this->newReceiptsView(); + + $column_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $external_view, + $invoices_view, + $receipts_view, + )); + + return $this->newPage() + ->setCrumbs($crumbs) + ->setTitle( + array( + pht('Invoices and Receipts'), + $account->getName(), + )) + ->appendChild($column_view); + } + + private function newInvoicesView() { + $xviewer = $this->getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $invoices = id(new PhortuneCartQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(true) + ->execute(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Invoices')); + + $invoices_table = id(new PhortuneOrderTableView()) + ->setViewer($xviewer) + ->setCarts($invoices) + ->setIsInvoices(true); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($invoices_table); + } + + private function newReceiptsView() { + $xviewer = $this->getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $receipts = id(new PhortuneCartQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->needPurchases(true) + ->withInvoices(false) + ->execute(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Receipts')); + + $receipts_table = id(new PhortuneOrderTableView()) + ->setViewer($xviewer) + ->setCarts($receipts); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($receipts_table); + } + +} diff --git a/src/applications/phortune/query/PhortuneAccountEmailQuery.php b/src/applications/phortune/query/PhortuneAccountEmailQuery.php index 4494372ef6..81684d1fdd 100644 --- a/src/applications/phortune/query/PhortuneAccountEmailQuery.php +++ b/src/applications/phortune/query/PhortuneAccountEmailQuery.php @@ -6,6 +6,7 @@ final class PhortuneAccountEmailQuery private $ids; private $phids; private $accountPHIDs; + private $addressKeys; public function withIDs(array $ids) { $this->ids = $ids; @@ -22,6 +23,11 @@ final class PhortuneAccountEmailQuery return $this; } + public function withAddressKeys(array $keys) { + $this->addressKeys = $keys; + return $this; + } + public function newResultObject() { return new PhortuneAccountEmail(); } @@ -77,6 +83,13 @@ final class PhortuneAccountEmailQuery $this->accountPHIDs); } + if ($this->addressKeys !== null) { + $where[] = qsprintf( + $conn, + 'address.addressKey IN (%Ls)', + $this->addressKeys); + } + return $where; } diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php index 5f1ede8414..65c67c56d2 100644 --- a/src/applications/phortune/storage/PhortuneAccountEmail.php +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -78,6 +78,13 @@ final class PhortuneAccountEmail $this->getID()); } + public function getExternalURI() { + return urisprintf( + '/phortune/external/%s/%s/', + $this->getAddressKey(), + $this->getAccessKey()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/view/PhortuneOrderTableView.php b/src/applications/phortune/view/PhortuneOrderTableView.php index b4b8866160..28dd1e58b2 100644 --- a/src/applications/phortune/view/PhortuneOrderTableView.php +++ b/src/applications/phortune/view/PhortuneOrderTableView.php @@ -49,6 +49,7 @@ final class PhortuneOrderTableView extends AphrontView { $is_invoices = $this->getIsInvoices(); $is_merchant = $this->getIsMerchantView(); + $is_external = (!$viewer->getPHID()); $phids = array(); foreach ($carts as $cart) { @@ -69,14 +70,18 @@ final class PhortuneOrderTableView extends AphrontView { if (count($purchases) == 1) { $purchase = head($purchases); - $purchase_name = $handles[$purchase->getPHID()]->renderLink(); + $purchase_name = $handles[$purchase->getPHID()]->getName(); $purchases = array(); } else { $purchase_name = ''; } if ($is_invoices) { - $merchant_link = $handles[$cart->getMerchantPHID()]->renderLink(); + if ($is_external) { + $merchant_link = $handles[$cart->getMerchantPHID()]->getName(); + } else { + $merchant_link = $handles[$cart->getMerchantPHID()]->renderLink(); + } } else { $merchant_link = null; } @@ -97,13 +102,12 @@ final class PhortuneOrderTableView extends AphrontView { PhortuneCart::getNameForStatus($cart->getStatus()), phabricator_datetime($cart->getDateModified(), $viewer), phabricator_datetime($cart->getDateCreated(), $viewer), - phutil_tag( - 'a', - array( - 'href' => $cart->getCheckoutURI(), - 'class' => 'small button button-green', - ), - pht('Pay Now')), + id(new PHUIButtonView()) + ->setTag('a') + ->setColor('green') + ->setHref($cart->getCheckoutURI()) + ->setText(pht('Pay Now')) + ->setIcon('fa-credit-card'), ); foreach ($purchases as $purchase) { $id = $purchase->getID(); @@ -164,7 +168,7 @@ final class PhortuneOrderTableView extends AphrontView { // We show "Pay Now" for due invoices, but not if the viewer is the // merchant, since it doesn't make sense for them to pay. - ($is_invoices && !$is_merchant), + ($is_invoices && !$is_merchant && !$is_external), )); return $table; From 4e13551e8546bf4036f352499a9893f3edba0455 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 23 Aug 2019 09:32:52 -0700 Subject: [PATCH 114/122] Add credential rotation and statuses (disabled, unsubscribed) to Phortune external email Summary: Depends on D20737. Ref T13367. Allow external addresses to have their access key rotated. Account managers can disable them, and anyone with the link can permanently unsubscribe them. Test Plan: Enabled/disabled addresses; permanently unsubscribed addresses. Maniphest Tasks: T13367 Differential Revision: https://secure.phabricator.com/D20738 --- src/__phutil_library_map__.php | 10 ++ .../PhabricatorPhortuneApplication.php | 8 +- .../PhortuneAccountEmailRotateController.php | 62 ++++++++ .../PhortuneAccountEmailStatusController.php | 137 ++++++++++++++++++ .../PhortuneAccountEmailViewController.php | 68 ++++++++- .../external/PhortuneExternalController.php | 24 ++- .../PhortuneExternalOverviewController.php | 9 +- .../PhortuneExternalUnsubscribeController.php | 67 +++++++++ .../phortune/storage/PhortuneAccountEmail.php | 7 + .../PhortuneAccountEmailRotateTransaction.php | 23 +++ .../PhortuneAccountEmailStatusTransaction.php | 23 +++ 11 files changed, 434 insertions(+), 4 deletions(-) create mode 100644 src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php create mode 100644 src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php create mode 100644 src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php create mode 100644 src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php create mode 100644 src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ec3ef2231b..c4be7187bd 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5237,7 +5237,11 @@ phutil_register_library_map(array( 'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php', 'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php', 'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php', + 'PhortuneAccountEmailRotateController' => 'applications/phortune/controller/account/PhortuneAccountEmailRotateController.php', + 'PhortuneAccountEmailRotateTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php', 'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php', + 'PhortuneAccountEmailStatusController' => 'applications/phortune/controller/account/PhortuneAccountEmailStatusController.php', + 'PhortuneAccountEmailStatusTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php', 'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php', 'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php', 'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php', @@ -5296,6 +5300,7 @@ phutil_register_library_map(array( 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php', 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', + 'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php', 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', @@ -11815,7 +11820,11 @@ phutil_register_library_map(array( 'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType', 'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneAccountEmailRotateController' => 'PhortuneAccountController', + 'PhortuneAccountEmailRotateTransaction' => 'PhortuneAccountEmailTransactionType', 'PhortuneAccountEmailStatus' => 'Phobject', + 'PhortuneAccountEmailStatusController' => 'PhortuneAccountController', + 'PhortuneAccountEmailStatusTransaction' => 'PhortuneAccountEmailTransactionType', 'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction', 'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType', @@ -11883,6 +11892,7 @@ phutil_register_library_map(array( 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneExternalController' => 'PhortuneController', 'PhortuneExternalOverviewController' => 'PhortuneExternalController', + 'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController', 'PhortuneInvoiceView' => 'AphrontTagView', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 43594c9653..9fc1a0ad01 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -87,7 +87,12 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { ), 'addresses/' => array( '' => 'PhortuneAccountEmailAddressesController', - '(?P\d+)/' => 'PhortuneAccountEmailViewController', + '(?P\d+)/' => array( + '' => 'PhortuneAccountEmailViewController', + 'rotate/' => 'PhortuneAccountEmailRotateController', + '(?Pdisable|enable)/' + => 'PhortuneAccountEmailStatusController', + ), $this->getEditRoutePattern('edit/') => 'PhortuneAccountEmailEditController', ), @@ -106,6 +111,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { ), 'external/(?P[^/]+)/(?P[^/]+)/' => array( '' => 'PhortuneExternalOverviewController', + 'unsubscribe/' => 'PhortuneExternalUnsubscribeController', ), 'merchant/' => array( $this->getQueryRoutePattern() diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php new file mode 100644 index 0000000000..2155ebcd20 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php @@ -0,0 +1,62 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $address_uri = $address->getURI(); + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $address->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailRotateTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $address->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($address_uri) + ->applyTransactions($address, $xactions); + + return id(new AphrontRedirectResponse())->setURI($address_uri); + } + + return $this->newDialog() + ->setTitle(pht('Rotate Access Key')) + ->appendParagraph( + pht( + 'Rotate the access key for email address %s?', + phutil_tag('strong', array(), $address->getAddress()))) + ->appendParagraph( + pht( + 'Existing access links which have been sent to this email address '. + 'will stop working.')) + ->addSubmitButton(pht('Rotate Access Key')) + ->addCancelButton($address_uri); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php new file mode 100644 index 0000000000..c11564b000 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php @@ -0,0 +1,137 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $address_uri = $address->getURI(); + + $is_enable = false; + $is_disable = false; + + $old_status = $address->getStatus(); + switch ($request->getURIData('action')) { + case 'enable': + if ($old_status === PhortuneAccountEmailStatus::STATUS_ACTIVE) { + return $this->newDialog() + ->setTitle(pht('Already Enabled')) + ->appendParagraph( + pht( + 'You can not enable this address because it is already '. + 'active.')) + ->addCancelButton($address_uri); + } + + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'You can not enable this address because it has been '. + 'permanently unsubscribed.')) + ->addCancelButton($address_uri); + } + + $new_status = PhortuneAccountEmailStatus::STATUS_ACTIVE; + $is_enable = true; + break; + case 'disable': + if ($old_status === PhortuneAccountEmailStatus::STATUS_DISABLED) { + return $this->newDialog() + ->setTitle(pht('Already Disabled')) + ->appendParagraph( + pht( + 'You can not disabled this address because it is already '. + 'disabled.')) + ->addCancelButton($address_uri); + } + + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'You can not disable this address because it has been '. + 'permanently unsubscribed.')) + ->addCancelButton($address_uri); + } + + $new_status = PhortuneAccountEmailStatus::STATUS_DISABLED; + $is_disable = true; + break; + default: + return new Aphront404Response(); + } + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $address->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + $address->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($address_uri) + ->applyTransactions($address, $xactions); + + return id(new AphrontRedirectResponse())->setURI($address_uri); + } + + $dialog = $this->newDialog(); + + $body = array(); + + if ($is_disable) { + $title = pht('Disable Address'); + + $body[] = pht( + 'This address will no longer receive email, and access links will '. + 'no longer function.'); + + $submit = pht('Disable Address'); + } else { + $title = pht('Enable Address'); + + $body[] = pht( + 'This address will receive email again, and existing links '. + 'to access order history will work again.'); + + $submit = pht('Enable Address'); + } + + foreach ($body as $graph) { + $dialog->appendParagraph($graph); + } + + return $dialog + ->setTitle($title) + ->addCancelButton($address_uri) + ->addSubmitButton($submit); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php index 97d90946f5..4e15210890 100644 --- a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -14,7 +14,7 @@ final class PhortuneAccountEmailViewController $address = id(new PhortuneAccountEmailQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) - ->withIDs(array($request->getURIData('id'))) + ->withIDs(array($request->getURIData('addressID'))) ->executeOne(); if (!$address) { return new Aphront404Response(); @@ -83,6 +83,56 @@ final class PhortuneAccountEmailViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + switch ($address->getStatus()) { + case PhortuneAccountEmailStatus::STATUS_ACTIVE: + $disable_name = pht('Disable Address'); + $disable_icon = 'fa-times'; + $can_disable = true; + $disable_action = 'disable'; + break; + case PhortuneAccountEmailStatus::STATUS_DISABLED: + $disable_name = pht('Enable Address'); + $disable_icon = 'fa-check'; + $can_disable = true; + $disable_action = 'enable'; + break; + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: + $disable_name = pht('Disable Address'); + $disable_icon = 'fa-times'; + $can_disable = false; + $disable_action = 'disable'; + break; + } + + $disable_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/%d/%s/', + $account->getID(), + $address->getID(), + $disable_action)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setDisabled(!$can_disable) + ->setWorkflow(true)); + + $rotate_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/%d/rotate/', + $account->getID(), + $address->getID())); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Rotate Access Key')) + ->setIcon('fa-refresh') + ->setHref($rotate_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show External View')) @@ -100,7 +150,23 @@ final class PhortuneAccountEmailViewController $view = id(new PHUIPropertyListView()) ->setUser($viewer); + $access_key = $address->getAccessKey(); + + // This is not a meaningful security barrier: the full plaintext of the + // access key is visible on the page in the link target of the "Show + // External View" action. It's just here to make it clear "Rotate Access + // Key" actually does something. + + $prefix_length = 4; + $visible_part = substr($access_key, 0, $prefix_length); + $masked_part = str_repeat( + "\xE2\x80\xA2", + strlen($access_key) - $prefix_length); + $access_display = $visible_part.$masked_part; + $access_display = phutil_tag('tt', array(), $access_display); + $view->addProperty(pht('Email Address'), $address->getAddress()); + $view->addProperty(pht('Access Key'), $access_display); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Email Address Details')) diff --git a/src/applications/phortune/controller/external/PhortuneExternalController.php b/src/applications/phortune/controller/external/PhortuneExternalController.php index 72ea02a955..a9ae254499 100644 --- a/src/applications/phortune/controller/external/PhortuneExternalController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalController.php @@ -83,7 +83,29 @@ abstract class PhortuneExternalController return $dialog; } - // TODO: Test that status is good. + switch ($email->getStatus()) { + case PhortuneAccountEmailStatus::STATUS_ACTIVE: + break; + case PhortuneAccountEmailStatus::STATUS_DISABLED: + return $this->newDialog() + ->setTitle(pht('Address Disabled')) + ->appendParagraph( + pht( + 'This email address (%s) has been disabled and no longer has '. + 'access to this payment account.', + $email_display)); + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'This email address (%s) has been permanently unsubscribed '. + 'and no longer has access to this payment account.', + $email_display)); + break; + default: + return new Aphront404Response(); + } $this->email = $email; diff --git a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php index bcd68e2f4a..ec360d4e60 100644 --- a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php @@ -12,7 +12,14 @@ final class PhortuneExternalOverviewController ->setBorder(true); $header = id(new PHUIHeaderView()) - ->setHeader(pht('Invoices and Receipts: %s', $account->getName())); + ->setHeader(pht('Invoices and Receipts: %s', $account->getName())) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-times') + ->setText(pht('Unsubscribe')) + ->setHref($email->getUnsubscribeURI()) + ->setWorkflow(true)); $external_view = $this->newExternalView(); $invoices_view = $this->newInvoicesView(); diff --git a/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php b/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php new file mode 100644 index 0000000000..c7c29129d3 --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php @@ -0,0 +1,67 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $email_uri = $email->getExternalURI(); + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $email->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) + ->setNewValue(PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED); + + $email->getApplicationTransactionEditor() + ->setActor($xviewer) + ->setActingAsPHID($email->getPHID()) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($email_uri) + ->applyTransactions($email, $xactions); + + return id(new AphrontRedirectResponse())->setURI($email_uri); + } + + $email_display = phutil_tag( + 'strong', + array(), + $email->getAddress()); + + $account_display = phutil_tag( + 'strong', + array(), + $account->getName()); + + $submit = pht( + 'Permanently Unsubscribe (%s)', + $email->getAddress()); + + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribe')) + ->appendParagraph( + pht( + 'Permanently unsubscribe this email address (%s) from this '. + 'payment account (%s)?', + $email_display, + $account_display)) + ->appendParagraph( + pht( + 'You will no longer receive email and access links will no longer '. + 'function.')) + ->appendParagraph( + pht( + 'This action is permanent and can not be undone.')) + ->addCancelButton($email_uri) + ->addSubmitButton($submit); + + } + +} diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php index 65c67c56d2..c33f92928c 100644 --- a/src/applications/phortune/storage/PhortuneAccountEmail.php +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -85,6 +85,13 @@ final class PhortuneAccountEmail $this->getAccessKey()); } + public function getUnsubscribeURI() { + return urisprintf( + '/phortune/external/%s/%s/unsubscribe/', + $this->getAddressKey(), + $this->getAccessKey()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php new file mode 100644 index 0000000000..e885b2d67b --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php @@ -0,0 +1,23 @@ +setAccessKey($access_key); + } + + public function getTitle() { + return pht( + '%s rotated the access key for this email address.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php new file mode 100644 index 0000000000..e607db2f99 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php @@ -0,0 +1,23 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + return pht( + '%s changed the status for this address to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } + +} From a0a38797127f091db7cdc3c782fc693206937183 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 23 Aug 2019 13:07:45 -0700 Subject: [PATCH 115/122] In Phortune, send order email to account external addresses Summary: Depends on D20738. Ref T13366. Fixes T8389. Now that the infrastructure is in place, actually send email to external addresses. Test Plan: Used `bin/phortune invoice` to generate invoices and saw associated external accounts receive mail in `bin/mail list-outbound`. Maniphest Tasks: T13366, T8389 Differential Revision: https://secure.phabricator.com/D20739 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPhortuneApplication.php | 3 + .../external/PhortuneExternalController.php | 25 ++--- .../PhortuneExternalOrderController.php | 40 ++++++++ .../PhortuneExternalOverviewController.php | 3 + .../phortune/editor/PhortuneCartEditor.php | 97 +++++++++++++++++++ .../query/PhortuneAccountEmailQuery.php | 13 +++ .../phortune/storage/PhortuneAccountEmail.php | 8 ++ .../phortune/storage/PhortuneCart.php | 4 + .../phortune/view/PhortuneOrderTableView.php | 25 ++++- .../worker/PhortuneSubscriptionWorker.php | 1 + ...habricatorApplicationTransactionEditor.php | 8 ++ 12 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 src/applications/phortune/controller/external/PhortuneExternalOrderController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c4be7187bd..97de6148f6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5299,6 +5299,7 @@ phutil_register_library_map(array( 'PhortuneDisplayException' => 'applications/phortune/exception/PhortuneDisplayException.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php', + 'PhortuneExternalOrderController' => 'applications/phortune/controller/external/PhortuneExternalOrderController.php', 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', 'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php', 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', @@ -11891,6 +11892,7 @@ phutil_register_library_map(array( 'PhortuneDisplayException' => 'Exception', 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneExternalController' => 'PhortuneController', + 'PhortuneExternalOrderController' => 'PhortuneExternalController', 'PhortuneExternalOverviewController' => 'PhortuneExternalController', 'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController', 'PhortuneInvoiceView' => 'AphrontTagView', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 9fc1a0ad01..fbc168eb58 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -112,6 +112,9 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'external/(?P[^/]+)/(?P[^/]+)/' => array( '' => 'PhortuneExternalOverviewController', 'unsubscribe/' => 'PhortuneExternalUnsubscribeController', + 'order/' => array( + '(?P[^/]+)/' => 'PhortuneExternalOrderController', + ), ), 'merchant/' => array( $this->getQueryRoutePattern() diff --git a/src/applications/phortune/controller/external/PhortuneExternalController.php b/src/applications/phortune/controller/external/PhortuneExternalController.php index a9ae254499..2810142531 100644 --- a/src/applications/phortune/controller/external/PhortuneExternalController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalController.php @@ -127,19 +127,11 @@ abstract class PhortuneExternalController $crumb = id(new PHUICrumbView()) ->setIcon('fa-diamond') - ->setName($crumb_name); - - $can_see = PhabricatorPolicyFilter::hasCapability( - $viewer, - $account, - PhabricatorPolicyCapability::CAN_VIEW); - if ($can_see) { - $crumb->setHref($account->getURI()); - } + ->setName($crumb_name) + ->setHref($email->getExternalURI()); $crumbs - ->addCrumb($crumb) - ->addTextCrumb(pht('Viewing As "%s"', $email->getAddress())); + ->addCrumb($crumb); } else { $crumb = id(new PHUICrumbView()) ->setIcon('fa-diamond') @@ -153,11 +145,22 @@ abstract class PhortuneExternalController final protected function newExternalView() { $email = $this->getAccountEmail(); + $xviewer = $this->getExternalViewer(); + + $origin_phid = $email->getAuthorPHID(); + + $handles = $xviewer->loadHandles(array($origin_phid)); + $messages = array(); $messages[] = pht( 'You are viewing this payment account as: %s', phutil_tag('strong', array(), $email->getAddress())); + + $messages[] = pht( + 'This email address was added to this payment account by: %s', + phutil_tag('strong', array(), $handles[$origin_phid]->getFullName())); + $messages[] = pht( 'Anyone who has a link to this page can view order history for '. 'this payment account.'); diff --git a/src/applications/phortune/controller/external/PhortuneExternalOrderController.php b/src/applications/phortune/controller/external/PhortuneExternalOrderController.php new file mode 100644 index 0000000000..82b231d344 --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalOrderController.php @@ -0,0 +1,40 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $order = id(new PhortuneCartQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('orderID'))) + ->executeOne(); + if (!$order) { + return new Aphront404Response(); + } + + $timeline = $this->buildTransactionTimeline( + $order, + new PhortuneCartTransactionQuery()); + $timeline->setShouldTerminate(true); + + $crumbs = $this->newExternalCrumbs() + ->addTextCrumb($order->getObjectName()); + + $view = id(new PHUITwoColumnView()) + ->setMainColumn( + array( + $timeline, + )); + + return $this->newPage() + ->setTitle(pht('Order %d', $order->getID())) + ->setCrumbs($crumbs) + ->appendChild($view); + } + +} diff --git a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php index ec360d4e60..5db74ac75e 100644 --- a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php @@ -9,6 +9,7 @@ final class PhortuneExternalOverviewController $account = $email->getAccount(); $crumbs = $this->newExternalCrumbs() + ->addTextCrumb(pht('Viewing As "%s"', $email->getAddress())) ->setBorder(true); $header = id(new PHUIHeaderView()) @@ -61,6 +62,7 @@ final class PhortuneExternalOverviewController $invoices_table = id(new PhortuneOrderTableView()) ->setViewer($xviewer) + ->setAccountEmail($email) ->setCarts($invoices) ->setIsInvoices(true); @@ -87,6 +89,7 @@ final class PhortuneExternalOverviewController $receipts_table = id(new PhortuneOrderTableView()) ->setViewer($xviewer) + ->setAccountEmail($email) ->setCarts($receipts); return id(new PHUIObjectBoxView()) diff --git a/src/applications/phortune/editor/PhortuneCartEditor.php b/src/applications/phortune/editor/PhortuneCartEditor.php index f95ea2450a..aef59f059a 100644 --- a/src/applications/phortune/editor/PhortuneCartEditor.php +++ b/src/applications/phortune/editor/PhortuneCartEditor.php @@ -246,5 +246,102 @@ final class PhortuneCartEditor return $xactions; } + protected function newAuxiliaryMail($object, array $xactions) { + $xviewer = PhabricatorUser::getOmnipotentUser(); + $account = $object->getAccount(); + + $addresses = id(new PhortuneAccountEmailQuery()) + ->setViewer($xviewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withStatuses( + array( + PhortuneAccountEmailStatus::STATUS_ACTIVE, + )) + ->execute(); + + $messages = array(); + foreach ($addresses as $address) { + $message = $this->newExternalMail($address, $object, $xactions); + if ($message) { + $messages[] = $message; + } + } + + return $messages; + } + + private function newExternalMail( + PhortuneAccountEmail $email, + PhortuneCart $cart, + array $xactions) { + $xviewer = PhabricatorUser::getOmnipotentUser(); + $account = $cart->getAccount(); + + $id = $cart->getID(); + $name = $cart->getName(); + + $origin_user = id(new PhabricatorPeopleQuery()) + ->setViewer($xviewer) + ->withPHIDs(array($email->getAuthorPHID())) + ->executeOne(); + if (!$origin_user) { + return null; + } + + if ($this->isInvoice()) { + $subject = pht('[Invoice #%d] %s', $id, $name); + $order_header = pht('INVOICE DETAIL'); + } else { + $subject = pht('[Order #%d] %s', $id, $name); + $order_header = pht('ORDER DETAIL'); + } + + $body = id(new PhabricatorMetaMTAMailBody()) + ->setViewer($xviewer) + ->setContextObject($cart); + + $origin_username = $origin_user->getUsername(); + $origin_realname = $origin_user->getRealName(); + if (strlen($origin_realname)) { + $origin_display = pht('%s (%s)', $origin_username, $origin_realname); + } else { + $origin_display = pht('%s', $origin_username); + } + + $body->addRawSection( + pht( + 'This email address (%s) was added to a payment account (%s) '. + 'by %s.', + $email->getAddress(), + $account->getName(), + $origin_display)); + + $body->addLinkSection( + $order_header, + PhabricatorEnv::getProductionURI($email->getExternalOrderURI($cart))); + + $body->addLinkSection( + pht('FULL ORDER HISTORY'), + PhabricatorEnv::getProductionURI($email->getExternalURI())); + + $body->addLinkSection( + pht('UNSUBSCRIBE'), + PhabricatorEnv::getProductionURI($email->getUnsubscribeURI())); + + return id(new PhabricatorMetaMTAMail()) + ->setFrom($this->getActingAsPHID()) + ->setSubject($subject) + ->addRawTos( + array( + $email->getAddress(), + )) + ->setForceDelivery(true) + ->setIsBulk(true) + ->setSensitiveContent(true) + ->setBody($body->render()) + ->setHTMLBody($body->renderHTML()); + + } + } diff --git a/src/applications/phortune/query/PhortuneAccountEmailQuery.php b/src/applications/phortune/query/PhortuneAccountEmailQuery.php index 81684d1fdd..0bdfdb78dc 100644 --- a/src/applications/phortune/query/PhortuneAccountEmailQuery.php +++ b/src/applications/phortune/query/PhortuneAccountEmailQuery.php @@ -7,6 +7,7 @@ final class PhortuneAccountEmailQuery private $phids; private $accountPHIDs; private $addressKeys; + private $statuses; public function withIDs(array $ids) { $this->ids = $ids; @@ -28,6 +29,11 @@ final class PhortuneAccountEmailQuery return $this; } + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function newResultObject() { return new PhortuneAccountEmail(); } @@ -90,6 +96,13 @@ final class PhortuneAccountEmailQuery $this->addressKeys); } + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'address.status IN (%Ls)', + $this->statuses); + } + return $where; } diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php index c33f92928c..e6761500cb 100644 --- a/src/applications/phortune/storage/PhortuneAccountEmail.php +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -92,6 +92,14 @@ final class PhortuneAccountEmail $this->getAccessKey()); } + public function getExternalOrderURI(PhortuneCart $cart) { + return urisprintf( + '/phortune/external/%s/%s/order/%d/', + $this->getAddressKey(), + $this->getAccessKey(), + $cart->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 624a13d6b0..91171037c6 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -656,6 +656,10 @@ final class PhortuneCart extends PhortuneDAO return idx($this->metadata, $key, $default); } + public function getObjectName() { + return pht('Order %d', $this->getID()); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/phortune/view/PhortuneOrderTableView.php b/src/applications/phortune/view/PhortuneOrderTableView.php index 28dd1e58b2..d7868f8d49 100644 --- a/src/applications/phortune/view/PhortuneOrderTableView.php +++ b/src/applications/phortune/view/PhortuneOrderTableView.php @@ -6,6 +6,7 @@ final class PhortuneOrderTableView extends AphrontView { private $noDataString; private $isInvoices; private $isMerchantView; + private $accountEmail; public function setCarts(array $carts) { $this->carts = $carts; @@ -43,13 +44,24 @@ final class PhortuneOrderTableView extends AphrontView { return $this->isMerchantView; } + public function setAccountEmail(PhortuneAccountEmail $account_email) { + $this->accountEmail = $account_email; + return $this; + } + + public function getAccountEmail() { + return $this->accountEmail; + } + public function render() { $carts = $this->getCarts(); $viewer = $this->getUser(); $is_invoices = $this->getIsInvoices(); $is_merchant = $this->getIsMerchantView(); - $is_external = (!$viewer->getPHID()); + $is_external = (bool)$this->getAccountEmail(); + + $email = $this->getAccountEmail(); $phids = array(); foreach ($carts as $cart) { @@ -65,7 +77,16 @@ final class PhortuneOrderTableView extends AphrontView { $rows = array(); $rowc = array(); foreach ($carts as $cart) { - $cart_link = $handles[$cart->getPHID()]->renderLink(); + if ($is_external) { + $cart_link = phutil_tag( + 'a', + array( + 'href' => $email->getExternalOrderURI($cart), + ), + $handles[$cart->getPHID()]->getName()); + } else { + $cart_link = $handles[$cart->getPHID()]->renderLink(); + } $purchases = $cart->getPurchases(); if (count($purchases) == 1) { diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php index 1c9a3f0fa0..438ebf5b1b 100644 --- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php +++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php @@ -22,6 +22,7 @@ final class PhortuneSubscriptionWorker extends PhabricatorWorker { return; } + $account = $subscription->getAccount(); $merchant = $subscription->getMerchant(); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 01294e308a..c17ac1ec72 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1520,6 +1520,10 @@ abstract class PhabricatorApplicationTransactionEditor } } + foreach ($this->newAuxiliaryMail($object, $xactions) as $message) { + $messages[] = $message; + } + // NOTE: This actually sends the mail. We do this last to reduce the chance // that we send some mail, hit an exception, then send the mail again when // retrying. @@ -4799,6 +4803,10 @@ abstract class PhabricatorApplicationTransactionEditor return $extensions; } + protected function newAuxiliaryMail($object, array $xactions) { + return array(); + } + private function generateMailStamps($object, $data) { if (!$data || !is_array($data)) { return null; From 97a4a59cf2c7f3fcf8cf013655cab4b4185a99b8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 24 Aug 2019 09:43:27 -0700 Subject: [PATCH 116/122] Give the Phortune external portal an order view Summary: Depends on D20739. Ref T13366. Slightly modularize/update components of order views, and make orders viewable from either an account context (existing view) or an external context (new view). The new view is generally simpler so this mostly just reorganizes existing code. Test Plan: Viewed orders as an account owner and an external user. Maniphest Tasks: T13366 Differential Revision: https://secure.phabricator.com/D20740 --- src/__phutil_library_map__.php | 10 +- .../base/controller/PhabricatorController.php | 13 +- .../PhabricatorPhortuneApplication.php | 5 +- .../cart/PhortuneCartCheckoutController.php | 15 +- .../cart/PhortuneCartController.php | 57 --- .../cart/PhortuneCartViewController.php | 287 +++----------- .../PhortuneExternalOrderController.php | 88 ++++- .../phortune/storage/PhortuneAccountEmail.php | 8 + .../phortune/view/PhortuneInvoiceView.php | 159 -------- .../view/PhortuneOrderDescriptionView.php | 39 ++ .../phortune/view/PhortuneOrderItemsView.php | 58 +++ .../view/PhortuneOrderSummaryView.php | 370 ++++++++++++++++++ .../phortune/view/PhortuneOrderView.php | 17 + 13 files changed, 663 insertions(+), 463 deletions(-) delete mode 100644 src/applications/phortune/view/PhortuneInvoiceView.php create mode 100644 src/applications/phortune/view/PhortuneOrderDescriptionView.php create mode 100644 src/applications/phortune/view/PhortuneOrderItemsView.php create mode 100644 src/applications/phortune/view/PhortuneOrderSummaryView.php create mode 100644 src/applications/phortune/view/PhortuneOrderView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 97de6148f6..81ac776d5d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5302,7 +5302,6 @@ phutil_register_library_map(array( 'PhortuneExternalOrderController' => 'applications/phortune/controller/external/PhortuneExternalOrderController.php', 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', 'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php', - 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', 'PhortuneMemberHasMerchantEdgeType' => 'applications/phortune/edge/PhortuneMemberHasMerchantEdgeType.php', @@ -5343,7 +5342,11 @@ phutil_register_library_map(array( 'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php', 'PhortuneMerchantTransactionType' => 'applications/phortune/xaction/PhortuneMerchantTransactionType.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', + 'PhortuneOrderDescriptionView' => 'applications/phortune/view/PhortuneOrderDescriptionView.php', + 'PhortuneOrderItemsView' => 'applications/phortune/view/PhortuneOrderItemsView.php', + 'PhortuneOrderSummaryView' => 'applications/phortune/view/PhortuneOrderSummaryView.php', 'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php', + 'PhortuneOrderView' => 'applications/phortune/view/PhortuneOrderView.php', 'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php', 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php', @@ -11895,7 +11898,6 @@ phutil_register_library_map(array( 'PhortuneExternalOrderController' => 'PhortuneExternalController', 'PhortuneExternalOverviewController' => 'PhortuneExternalController', 'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController', - 'PhortuneInvoiceView' => 'AphrontTagView', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', 'PhortuneMemberHasMerchantEdgeType' => 'PhabricatorEdgeType', @@ -11940,7 +11942,11 @@ phutil_register_library_map(array( 'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneMerchantTransactionType' => 'PhabricatorModularTransactionType', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', + 'PhortuneOrderDescriptionView' => 'AphrontView', + 'PhortuneOrderItemsView' => 'PhortuneOrderView', + 'PhortuneOrderSummaryView' => 'PhortuneOrderView', 'PhortuneOrderTableView' => 'AphrontView', + 'PhortuneOrderView' => 'AphrontView', 'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider', 'PhortunePaymentMethod' => array( 'PhortuneDAO', diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index cfd0eaee65..a463c741d1 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -481,7 +481,7 @@ abstract class PhabricatorController extends AphrontController { protected function buildTransactionTimeline( PhabricatorApplicationTransactionInterface $object, - PhabricatorApplicationTransactionQuery $query, + PhabricatorApplicationTransactionQuery $query = null, PhabricatorMarkupEngine $engine = null, $view_data = array()) { @@ -489,6 +489,17 @@ abstract class PhabricatorController extends AphrontController { $viewer = $this->getViewer(); $xaction = $object->getApplicationTransactionTemplate(); + if (!$query) { + $query = PhabricatorApplicationTransactionQuery::newQueryForObject( + $object); + if (!$query) { + throw new Exception( + pht( + 'Unable to find transaction query for object of class "%s".', + get_class($object))); + } + } + $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request) ->setURI(new PhutilURI( diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index fbc168eb58..79b246770e 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -113,7 +113,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { '' => 'PhortuneExternalOverviewController', 'unsubscribe/' => 'PhortuneExternalUnsubscribeController', 'order/' => array( - '(?P[^/]+)/' => 'PhortuneExternalOrderController', + '(?P[^/]+)/' => array( + '' => 'PhortuneExternalOrderController', + '(?Pprint)/' => 'PhortuneExternalOrderController', + ), ), ), 'merchant/' => array( diff --git a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php index 4b59afef75..793187c404 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php @@ -207,7 +207,9 @@ final class PhortuneCartCheckoutController ->appendChild($form) ->appendChild($provider_form); - $description_box = $this->renderCartDescription($cart); + $description_view = id(new PhortuneOrderDescriptionView()) + ->setViewer($viewer) + ->setOrder($cart); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Checkout')); @@ -220,11 +222,12 @@ final class PhortuneCartCheckoutController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $cart_box, - $description_box, - $payment_box, - )); + ->setFooter( + array( + $description_view, + $cart_box, + $payment_box, + )); return $this->newPage() ->setTitle($title) diff --git a/src/applications/phortune/controller/cart/PhortuneCartController.php b/src/applications/phortune/controller/cart/PhortuneCartController.php index 2f5f55014e..c28581bc47 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartController.php @@ -74,61 +74,4 @@ abstract class PhortuneCartController PhabricatorPolicyCapability::CAN_EDIT); } - protected function buildCartContentTable(PhortuneCart $cart) { - - $rows = array(); - foreach ($cart->getPurchases() as $purchase) { - $rows[] = array( - $purchase->getFullDisplayName(), - $purchase->getBasePriceAsCurrency()->formatForDisplay(), - $purchase->getQuantity(), - $purchase->getTotalPriceAsCurrency()->formatForDisplay(), - ); - } - - $rows[] = array( - phutil_tag('strong', array(), pht('Total')), - '', - '', - phutil_tag('strong', array(), - $cart->getTotalPriceAsCurrency()->formatForDisplay()), - ); - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - pht('Item'), - pht('Price'), - pht('Qty.'), - pht('Total'), - )); - $table->setColumnClasses( - array( - 'wide', - 'right', - 'right', - 'right', - )); - - return $table; - } - - protected function renderCartDescription(PhortuneCart $cart) { - $description = $cart->getDescription(); - if (!strlen($description)) { - return null; - } - - $output = new PHUIRemarkupView($this->getViewer(), $description); - - $box = id(new PHUIBoxView()) - ->addMargin(PHUI::MARGIN_LARGE) - ->appendChild($output); - - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Description')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($box); - } - } diff --git a/src/applications/phortune/controller/cart/PhortuneCartViewController.php b/src/applications/phortune/controller/cart/PhortuneCartViewController.php index d728a202c9..9412d11045 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartViewController.php @@ -3,8 +3,6 @@ final class PhortuneCartViewController extends PhortuneCartController { - private $action = null; - protected function shouldRequireAccountAuthority() { return false; } @@ -15,214 +13,93 @@ final class PhortuneCartViewController protected function handleCartRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $cart = $this->getCart(); + $order = $this->getCart(); $authority = $this->getMerchantAuthority(); $can_edit = $this->hasAccountAuthority(); - $this->action = $request->getURIData('action'); + $is_printable = ($request->getURIData('action') === 'print'); - $cart_table = $this->buildCartContentTable($cart); - - $errors = array(); - $error_view = null; $resume_uri = null; - switch ($cart->getStatus()) { - case PhortuneCart::STATUS_READY: - if ($cart->getIsInvoice()) { - $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) - ->appendChild(pht('This invoice is ready for payment.')); - } - break; - case PhortuneCart::STATUS_PURCHASING: - if ($can_edit) { - $resume_uri = $cart->getMetadataValue('provider.checkoutURI'); - if ($resume_uri) { - $errors[] = pht( - 'The checkout process has been started, but not yet completed. '. - 'You can continue checking out by clicking %s, or cancel the '. - 'order, or contact the merchant for assistance.', - phutil_tag('strong', array(), pht('Continue Checkout'))); - } else { - $errors[] = pht( - 'The checkout process has been started, but an error occurred. '. - 'You can cancel the order or contact the merchant for '. - 'assistance.'); - } - } - break; - case PhortuneCart::STATUS_CHARGED: - if ($can_edit) { - $errors[] = pht( - 'You have been charged, but processing could not be completed. '. - 'You can cancel your order, or contact the merchant for '. - 'assistance.'); - } - break; - case PhortuneCart::STATUS_HOLD: - if ($can_edit) { - $errors[] = pht( - 'Payment for this order is on hold. You can click %s to check '. - 'for updates, cancel the order, or contact the merchant for '. - 'assistance.', - phutil_tag('strong', array(), pht('Update Status'))); - } - break; - case PhortuneCart::STATUS_REVIEW: - if ($authority) { - $errors[] = pht( - 'This order has been flagged for manual review. Review the order '. - 'and choose %s to accept it or %s to reject it.', - phutil_tag('strong', array(), pht('Accept Order')), - phutil_tag('strong', array(), pht('Refund Order'))); - } else if ($can_edit) { - $errors[] = pht( - 'This order requires manual processing and will complete once '. - 'the merchant accepts it.'); - } - break; - case PhortuneCart::STATUS_PURCHASED: - $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_SUCCESS) - ->appendChild(pht('This purchase has been completed.')); - break; + if ($order->getStatus() === PhortuneCart::STATUS_PURCHASING) { + if ($can_edit) { + $resume_uri = $order->getMetadataValue('provider.checkoutURI'); + } } - if ($errors) { - $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->appendChild($errors); - } - - $details = $this->buildDetailsView($cart); - $curtain = $this->buildCurtainView( - $cart, - $can_edit, - $authority, - $resume_uri); - $header = id(new PHUIHeaderView()) ->setUser($viewer) - ->setHeader($cart->getName()) + ->setHeader($order->getName()) ->setHeaderIcon('fa-shopping-bag'); - if ($cart->getStatus() == PhortuneCart::STATUS_PURCHASED) { - $done_uri = $cart->getDoneURI(); + if ($order->getStatus() == PhortuneCart::STATUS_PURCHASED) { + $done_uri = $order->getDoneURI(); if ($done_uri) { $header->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($done_uri) ->setIcon('fa-check-square green') - ->setText($cart->getDoneActionName())); + ->setText($order->getDoneActionName())); } } - $cart_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Cart Items')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($cart_table); - - $description = $this->renderCartDescription($cart); - - $charges = id(new PhortuneChargeQuery()) + $order_view = id(new PhortuneOrderSummaryView()) ->setViewer($viewer) - ->withCartPHIDs(array($cart->getPHID())) - ->needCarts(true) - ->execute(); + ->setOrder($order) + ->setResumeURI($resume_uri) + ->setPrintable($is_printable); - $charges_table = id(new PhortuneChargeTableView()) - ->setUser($viewer) - ->setCharges($charges) - ->setShowOrder(false); + $crumbs = null; + $curtain = null; - $charges = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Charges')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($charges_table); + $main = array(); + $tail = array(); - $account = $cart->getAccount(); + require_celerity_resource('phortune-invoice-css'); - $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb($account->getName(), $account->getURI()) - ->addTextCrumb(pht('Orders'), $account->getOrdersURI()) - ->addTextCrumb(pht('Cart %d', $cart->getID())) - ->setBorder(true); - - require_celerity_resource('phortune-css'); - - if (!$this->action) { - $class = 'phortune-cart-page'; - $timeline = $this->buildTransactionTimeline( - $cart, - new PhortuneCartTransactionQuery()); - $timeline - ->setShouldTerminate(true); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setCurtain($curtain) - ->setMainColumn(array( - $error_view, - $details, - $cart_box, - $description, - $charges, - $timeline, - )); + if ($is_printable) { + $body_class = 'phortune-invoice-view'; + $tail[] = $order_view; } else { - $class = 'phortune-invoice-view'; - $crumbs = null; - $merchant_phid = $cart->getMerchantPHID(); - $buyer_phid = $cart->getAuthorPHID(); - $merchant = id(new PhortuneMerchantQuery()) - ->setViewer($viewer) - ->withPHIDs(array($merchant_phid)) - ->needProfileImage(true) - ->executeOne(); - $buyer = id(new PhabricatorPeopleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($buyer_phid)) - ->needProfileImage(true) - ->executeOne(); + $body_class = 'phortune-cart-page'; - $merchant_contact = new PHUIRemarkupView( - $viewer, - $merchant->getContactInfo()); + $curtain = $this->buildCurtainView( + $order, + $can_edit, + $authority, + $resume_uri); - $account_name = $account->getBillingName(); - if (!strlen($account_name)) { - $account_name = $buyer->getRealName(); - } + $account = $order->getAccount(); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($account->getName(), $account->getURI()) + ->addTextCrumb(pht('Orders'), $account->getOrdersURI()) + ->addTextCrumb($order->getObjectName()) + ->setBorder(true); - $account_contact = $account->getBillingAddress(); - if (strlen($account_contact)) { - $account_contact = new PHUIRemarkupView( - $viewer, - $account_contact); - } + $timeline = $this->buildTransactionTimeline($order) + ->setShouldTerminate(true); - $view = id(new PhortuneInvoiceView()) - ->setMerchantName($merchant->getName()) - ->setMerchantLogo($merchant->getProfileImageURI()) - ->setMerchantContact($merchant_contact) - ->setMerchantFooter($merchant->getInvoiceFooter()) - ->setAccountName($account_name) - ->setAccountContact($account_contact) - ->setStatus($error_view) - ->setContent( - array( - $details, - $cart_box, - $charges, - )); + $main[] = $order_view; + $main[] = $timeline; + } + + $column_view = id(new PHUITwoColumnView()) + ->setMainColumn($main) + ->setFooter($tail); + + if ($curtain) { + $column_view->setCurtain($curtain); } $page = $this->newPage() - ->setTitle(pht('Cart %d', $cart->getID())) - ->addClass($class) - ->appendChild($view); + ->addClass($body_class) + ->setTitle( + array( + $order->getObjectName(), + $order->getName(), + )) + ->appendChild($column_view); if ($crumbs) { $page->setCrumbs($crumbs); @@ -231,45 +108,6 @@ final class PhortuneCartViewController return $page; } - private function buildDetailsView(PhortuneCart $cart) { - $viewer = $this->getViewer(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($cart); - - $handles = $this->loadViewerHandles( - array( - $cart->getAccountPHID(), - $cart->getAuthorPHID(), - $cart->getMerchantPHID(), - )); - - if ($this->action == 'print') { - $view->addProperty(pht('Order Name'), $cart->getName()); - } - - $view->addProperty( - pht('Account'), - $handles[$cart->getAccountPHID()]->renderLink()); - $view->addProperty( - pht('Authorized By'), - $handles[$cart->getAuthorPHID()]->renderLink()); - $view->addProperty( - pht('Merchant'), - $handles[$cart->getMerchantPHID()]->renderLink()); - $view->addProperty( - pht('Status'), - PhortuneCart::getNameForStatus($cart->getStatus())); - $view->addProperty( - pht('Updated'), - phabricator_datetime($cart->getDateModified(), $viewer)); - - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Details')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($view); - } - private function buildCurtainView( PhortuneCart $cart, $can_edit, @@ -297,6 +135,18 @@ final class PhortuneCartViewController $checkout_uri = $cart->getCheckoutURI(); $void_uri = $this->getApplicationURI("cart/{$id}/void/"); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Printable Version')) + ->setHref($print_uri) + ->setOpenInNewWindow(true) + ->setIcon('fa-print')); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER)); + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Pay Now')) @@ -327,13 +177,6 @@ final class PhortuneCartViewController ->setHref($resume_uri)); } - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Printable Version')) - ->setHref($print_uri) - ->setOpenInNewWindow(true) - ->setIcon('fa-print')); - if ($authority) { $curtain->addAction( id(new PhabricatorActionView()) diff --git a/src/applications/phortune/controller/external/PhortuneExternalOrderController.php b/src/applications/phortune/controller/external/PhortuneExternalOrderController.php index 82b231d344..36522f95af 100644 --- a/src/applications/phortune/controller/external/PhortuneExternalOrderController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalOrderController.php @@ -17,24 +17,82 @@ final class PhortuneExternalOrderController return new Aphront404Response(); } - $timeline = $this->buildTransactionTimeline( - $order, - new PhortuneCartTransactionQuery()); - $timeline->setShouldTerminate(true); + $is_printable = ($request->getURIData('action') === 'print'); - $crumbs = $this->newExternalCrumbs() - ->addTextCrumb($order->getObjectName()); + $order_view = id(new PhortuneOrderSummaryView()) + ->setViewer($xviewer) + ->setOrder($order) + ->setPrintable($is_printable); - $view = id(new PHUITwoColumnView()) - ->setMainColumn( + $crumbs = null; + $curtain = null; + + $main = array(); + $tail = array(); + + require_celerity_resource('phortune-invoice-css'); + + if ($is_printable) { + $body_class = 'phortune-invoice-view'; + + $tail[] = $order_view; + } else { + $body_class = 'phortune-cart-page'; + + $curtain = $this->newCurtain($order); + + $crumbs = $this->newExternalCrumbs() + ->addTextCrumb($order->getObjectName()) + ->setBorder(true); + + $timeline = $this->buildTransactionTimeline($order) + ->setShouldTerminate(true); + + $main[] = $order_view; + $main[] = $timeline; + } + + $column_view = id(new PHUITwoColumnView()) + ->setMainColumn($main) + ->setFooter($tail); + + if ($curtain) { + $column_view->setCurtain($curtain); + } + + $page = $this->newPage() + ->addClass($body_class) + ->setTitle( array( - $timeline, - )); + $order->getObjectName(), + $order->getName(), + )) + ->appendChild($column_view); - return $this->newPage() - ->setTitle(pht('Order %d', $order->getID())) - ->setCrumbs($crumbs) - ->appendChild($view); - } + if ($crumbs) { + $page->setCrumbs($crumbs); + } + + return $page; + } + + + private function newCurtain(PhortuneCart $order) { + $xviewer = $this->getExternalViewer(); + $email = $this->getAccountEmail(); + + $curtain = $this->newCurtainView($order); + + $print_uri = $email->getExternalOrderPrintURI($order); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Printable Version')) + ->setHref($print_uri) + ->setOpenInNewWindow(true) + ->setIcon('fa-print')); + + return $curtain; + } } diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php index e6761500cb..50c8e90025 100644 --- a/src/applications/phortune/storage/PhortuneAccountEmail.php +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -100,6 +100,14 @@ final class PhortuneAccountEmail $cart->getID()); } + public function getExternalOrderPrintURI(PhortuneCart $cart) { + return urisprintf( + '/phortune/external/%s/%s/order/%d/print/', + $this->getAddressKey(), + $this->getAccessKey(), + $cart->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/view/PhortuneInvoiceView.php b/src/applications/phortune/view/PhortuneInvoiceView.php deleted file mode 100644 index 65da418cc2..0000000000 --- a/src/applications/phortune/view/PhortuneInvoiceView.php +++ /dev/null @@ -1,159 +0,0 @@ -merchantName = $name; - return $this; - } - - public function setMerchantLogo($logo) { - $this->merchantLogo = $logo; - return $this; - } - - public function setMerchantContact($contact) { - $this->merchantContact = $contact; - return $this; - } - - public function setMerchantFooter($footer) { - $this->merchantFooter = $footer; - return $this; - } - - public function setAccountName($name) { - $this->accountName = $name; - return $this; - } - - public function setAccountContact($contact) { - $this->accountContact = $contact; - return $this; - } - - public function setStatus($status) { - $this->status = $status; - return $this; - } - - public function setContent($content) { - $this->content = $content; - return $this; - } - - protected function getTagAttributes() { - $classes = array(); - $classes[] = 'phortune-invoice-view'; - - return array( - 'class' => implode(' ', $classes), - ); - } - - protected function getTagContent() { - require_celerity_resource('phortune-invoice-css'); - - $logo = phutil_tag( - 'div', - array( - 'class' => 'phortune-invoice-logo', - ), - phutil_tag( - 'img', - array( - 'height' => '50', - 'width' => '50', - 'alt' => $this->merchantName, - 'src' => $this->merchantLogo, - ))); - - $to_title = phutil_tag( - 'div', - array( - 'class' => 'phortune-mini-header', - ), - pht('Bill To:')); - - $bill_to = phutil_tag( - 'td', - array( - 'class' => 'phortune-invoice-to', - 'width' => '50%', - ), - array( - $to_title, - phutil_tag('strong', array(), $this->accountName), - phutil_tag('br', array()), - $this->accountContact, - )); - - $from_title = phutil_tag( - 'div', - array( - 'class' => 'phortune-mini-header', - ), - pht('From:')); - - $bill_from = phutil_tag( - 'td', - array( - 'class' => 'phortune-invoice-from', - 'width' => '50%', - ), - array( - $from_title, - phutil_tag('strong', array(), $this->merchantName), - phutil_tag('br', array()), - $this->merchantContact, - )); - - $contact = phutil_tag( - 'table', - array( - 'class' => 'phortune-invoice-contact', - 'width' => '100%', - ), - phutil_tag( - 'tr', - array(), - array( - $bill_to, - $bill_from, - ))); - - $status = null; - if ($this->status) { - $status = phutil_tag( - 'div', - array( - 'class' => 'phortune-invoice-status', - ), - $this->status); - } - - $footer = phutil_tag( - 'div', - array( - 'class' => 'phortune-invoice-footer', - ), - $this->merchantFooter); - - return array( - $logo, - $contact, - $status, - $this->content, - $footer, - ); - } -} diff --git a/src/applications/phortune/view/PhortuneOrderDescriptionView.php b/src/applications/phortune/view/PhortuneOrderDescriptionView.php new file mode 100644 index 0000000000..1eaf290341 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderDescriptionView.php @@ -0,0 +1,39 @@ +order = $order; + return $this; + } + + public function getOrder() { + return $this->order; + } + + public function render() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $description = $order->getDescription(); + if (!strlen($description)) { + return null; + } + + $output = new PHUIRemarkupView($viewer, $description); + + $description_box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($output); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Description')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($description_box); + } + + +} diff --git a/src/applications/phortune/view/PhortuneOrderItemsView.php b/src/applications/phortune/view/PhortuneOrderItemsView.php new file mode 100644 index 0000000000..8c7f06db13 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderItemsView.php @@ -0,0 +1,58 @@ +getViewer(); + $order = $this->getOrder(); + + $purchases = id(new PhortunePurchaseQuery()) + ->setViewer($viewer) + ->withCartPHIDs(array($order->getPHID())) + ->execute(); + + $order->attachPurchases($purchases); + + $rows = array(); + foreach ($purchases as $purchase) { + $rows[] = array( + $purchase->getFullDisplayName(), + $purchase->getBasePriceAsCurrency()->formatForDisplay(), + $purchase->getQuantity(), + $purchase->getTotalPriceAsCurrency()->formatForDisplay(), + ); + } + + $rows[] = array( + phutil_tag('strong', array(), pht('Total')), + '', + '', + phutil_tag('strong', array(), + $order->getTotalPriceAsCurrency()->formatForDisplay()), + ); + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + pht('Item'), + pht('Price'), + pht('Qty.'), + pht('Total'), + )); + $table->setColumnClasses( + array( + 'wide', + 'right', + 'right', + 'right', + )); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Items')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + +} diff --git a/src/applications/phortune/view/PhortuneOrderSummaryView.php b/src/applications/phortune/view/PhortuneOrderSummaryView.php new file mode 100644 index 0000000000..77152b9ff7 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderSummaryView.php @@ -0,0 +1,370 @@ +resumeURI = $resume_uri; + return $this; + } + + public function getResumeURI() { + return $this->resumeURI; + } + + public function setPrintable($printable) { + $this->printable = $printable; + return $this; + } + + public function getPrintable() { + return $this->printable; + } + + public function render() { + $is_printable = $this->getPrintable(); + + $content = array(); + + if ($is_printable) { + $content[] = $this->newContactHeader(); + } + + $content[] = $this->newMessagesView(); + $content[] = $this->newDetailsView(); + $content[] = $this->newDescriptionView(); + $content[] = $this->newItemsView(); + $content[] = $this->newChargesView(); + + if ($is_printable) { + $content[] = $this->newContactFooter(); + } + + return $content; + } + + private function newMessagesView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $messages = array(); + $severity = null; + + $resume_uri = $this->getResumeURI(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $order, + PhabricatorPolicyCapability::CAN_EDIT); + + $can_merchant = PhabricatorPolicyFilter::hasCapability( + $viewer, + $order->getMerchant(), + PhabricatorPolicyCapability::CAN_EDIT); + + switch ($order->getStatus()) { + case PhortuneCart::STATUS_READY: + if ($order->getIsInvoice()) { + $severity = PHUIInfoView::SEVERITY_NOTICE; + $messages[] = pht('This invoice is ready for payment.'); + } + break; + case PhortuneCart::STATUS_PURCHASING: + if ($can_edit) { + if ($resume_uri) { + $messages[] = pht( + 'The checkout process has been started, but not yet completed. '. + 'You can continue checking out by clicking %s, or cancel the '. + 'order, or contact the merchant for assistance.', + phutil_tag('strong', array(), pht('Continue Checkout'))); + } else { + $messages[] = pht( + 'The checkout process has been started, but an error occurred. '. + 'You can cancel the order or contact the merchant for '. + 'assistance.'); + } + } + break; + case PhortuneCart::STATUS_CHARGED: + if ($can_edit) { + $messages[] = pht( + 'You have been charged, but processing could not be completed. '. + 'You can cancel your order, or contact the merchant for '. + 'assistance.'); + } + break; + case PhortuneCart::STATUS_HOLD: + if ($can_edit) { + $messages[] = pht( + 'Payment for this order is on hold. You can click %s to check '. + 'for updates, cancel the order, or contact the merchant for '. + 'assistance.', + phutil_tag('strong', array(), pht('Update Status'))); + } + break; + case PhortuneCart::STATUS_REVIEW: + if ($can_merchant) { + $messages[] = pht( + 'This order has been flagged for manual review. Review the order '. + 'and choose %s to accept it or %s to reject it.', + phutil_tag('strong', array(), pht('Accept Order')), + phutil_tag('strong', array(), pht('Refund Order'))); + } else if ($can_edit) { + $messages[] = pht( + 'This order requires manual processing and will complete once '. + 'the merchant accepts it.'); + } + break; + case PhortuneCart::STATUS_PURCHASED: + $severity = PHUIInfoView::SEVERITY_SUCCESS; + $messages[] = pht('This purchase has been completed.'); + break; + } + + if (!$messages) { + return null; + } + + if ($severity === null) { + $severity = PHUIInfoView::SEVERITY_WARNING; + } + + $messages_view = id(new PHUIInfoView()) + ->setSeverity($severity) + ->appendChild($messages); + + $is_printable = $this->getPrintable(); + if ($is_printable) { + $messages_view = phutil_tag( + 'div', + array( + 'class' => 'phortune-invoice-status', + ), + $messages_view); + } + + return $messages_view; + } + + private function newDetailsView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + $is_printable = $this->getPrintable(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer) + ->setObject($order); + + $account_phid = $order->getAccountPHID(); + $author_phid = $order->getAuthorPHID(); + $merchant_phid = $order->getMerchantPHID(); + + $handles = $viewer->loadHandles( + array( + $account_phid, + $author_phid, + $merchant_phid, + )); + + if ($is_printable) { + $account_link = $handles[$account_phid]->getFullName(); + $author_link = $handles[$author_phid]->getFullName(); + $merchant_link = $handles[$merchant_phid]->getFullName(); + } else { + $account_link = $handles[$account_phid]->renderLink(); + $author_link = $handles[$author_phid]->renderLink(); + $merchant_link = $handles[$merchant_phid]->renderLink(); + } + + if ($is_printable) { + $view->addProperty(pht('Order Name'), $order->getName()); + } + + $view->addProperty(pht('Account'), $account_link); + $view->addProperty(pht('Authorized By'), $author_link); + $view->addProperty(pht('Merchant'), $merchant_link); + + $view->addProperty( + pht('Order Status'), + PhortuneCart::getNameForStatus($order->getStatus())); + $view->addProperty( + pht('Created'), + phabricator_datetime($order->getDateCreated(), $viewer)); + $view->addProperty( + pht('Updated'), + phabricator_datetime($order->getDateModified(), $viewer)); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($view); + } + + private function newChargesView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $charges = id(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withCartPHIDs(array($order->getPHID())) + ->needCarts(true) + ->execute(); + + $charges_table = id(new PhortuneChargeTableView()) + ->setUser($viewer) + ->setCharges($charges) + ->setShowOrder(false); + + $charges_view = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Charges')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($charges_table); + + return $charges_view; + } + + private function newDescriptionView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + return id(new PhortuneOrderDescriptionView()) + ->setViewer($viewer) + ->setOrder($order); + } + + private function newItemsView() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + return id(new PhortuneOrderItemsView()) + ->setViewer($viewer) + ->setOrder($order); + } + + private function newContactHeader() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs(array($order->getMerchant()->getPHID())) + ->needProfileImage(true) + ->executeOne(); + + $merchant_name = $merchant->getName(); + $merchant_image = $merchant->getProfileImageURI(); + + $account = $order->getAccount(); + $account_name = $account->getBillingName(); + + $account_contact = $account->getBillingAddress(); + if (strlen($account_contact)) { + $account_contact = new PHUIRemarkupView( + $viewer, + $account_contact); + } + + $merchant_contact = $merchant->getContactInfo(); + if (strlen($merchant_contact)) { + $merchant_contact = new PHUIRemarkupView( + $viewer, + $merchant->getContactInfo()); + } + + $logo = phutil_tag( + 'div', + array( + 'class' => 'phortune-invoice-logo', + ), + phutil_tag( + 'img', + array( + 'height' => '50', + 'width' => '50', + 'alt' => $merchant_name, + 'src' => $merchant_image, + ))); + + $to_title = phutil_tag( + 'div', + array( + 'class' => 'phortune-mini-header', + ), + pht('Bill To:')); + + $bill_to = phutil_tag( + 'td', + array( + 'class' => 'phortune-invoice-to', + 'width' => '50%', + ), + array( + $to_title, + phutil_tag('strong', array(), $account_name), + phutil_tag('br', array()), + $account_contact, + )); + + $from_title = phutil_tag( + 'div', + array( + 'class' => 'phortune-mini-header', + ), + pht('From:')); + + $bill_from = phutil_tag( + 'td', + array( + 'class' => 'phortune-invoice-from', + 'width' => '50%', + ), + array( + $from_title, + phutil_tag('strong', array(), $merchant_name), + phutil_tag('br', array()), + $merchant_contact, + )); + + $contact = phutil_tag( + 'table', + array( + 'class' => 'phortune-invoice-contact', + 'width' => '100%', + ), + phutil_tag( + 'tr', + array(), + array( + $bill_to, + $bill_from, + ))); + + return array( + $logo, + $contact, + ); + } + + private function newContactFooter() { + $viewer = $this->getViewer(); + $order = $this->getOrder(); + + $merchant = $order->getMerchant(); + $footer = $merchant->getInvoiceFooter(); + + if (!strlen($footer)) { + return null; + } + + return phutil_tag( + 'div', + array( + 'class' => 'phortune-invoice-footer', + ), + $footer); + } + +} diff --git a/src/applications/phortune/view/PhortuneOrderView.php b/src/applications/phortune/view/PhortuneOrderView.php new file mode 100644 index 0000000000..25261aa423 --- /dev/null +++ b/src/applications/phortune/view/PhortuneOrderView.php @@ -0,0 +1,17 @@ +order = $order; + return $this; + } + + final public function getOrder() { + return $this->order; + } + +} From 7198bd7db784d4a2dc4890106e586f0b607ce080 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 27 Aug 2019 15:27:25 -0700 Subject: [PATCH 117/122] When "utf8mb4" is available, use it as the default client charset when invoking standalone "mysql" commands Summary: Fixes T13390. We have some old code which doesn't dynamically select between "utf8mb4" and "utf8". This can lead to dumping utf8mb4 data over a utf8 connection in `bin/storage dump`, which possibly corrupts some emoji/whales. Instead, prefer "utf8mb4" if it's available. Test Plan: Ran `bin/storage dump` and `bin/storage shell`, saw sub-commands select utf8mb4 as the client charset. Maniphest Tasks: T13390 Differential Revision: https://secure.phabricator.com/D20742 --- .../management/PhabricatorStorageManagementAPI.php | 8 ++++++++ .../workflow/PhabricatorStorageManagementDumpWorkflow.php | 4 +++- .../PhabricatorStorageManagementShellWorkflow.php | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php index e66ba784f7..b838c8a5d9 100644 --- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php +++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php @@ -298,6 +298,14 @@ final class PhabricatorStorageManagementAPI extends Phobject { return self::isCharacterSetAvailableOnConnection($character_set, $conn); } + public function getClientCharset() { + if ($this->isCharacterSetAvailable('utf8mb4')) { + return 'utf8mb4'; + } else { + return 'utf8'; + } + } + public static function isCharacterSetAvailableOnConnection( $character_set, AphrontDatabaseConnection $conn) { diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index 28b188a873..3a18578a30 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -179,7 +179,9 @@ final class PhabricatorStorageManagementDumpWorkflow $argv = array(); $argv[] = '--hex-blob'; $argv[] = '--single-transaction'; - $argv[] = '--default-character-set=utf8'; + + $argv[] = '--default-character-set'; + $argv[] = $api->getClientCharset(); if ($args->getArg('for-replica')) { $argv[] = '--master-data'; diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php index 0bf185a086..f376ea3e14 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php @@ -31,8 +31,8 @@ final class PhabricatorStorageManagementShellWorkflow } return phutil_passthru( - 'mysql --protocol=TCP --default-character-set=utf8mb4 '. - '-u %s %C -h %s %C', + 'mysql --protocol=TCP --default-character-set %R -u %s %C -h %s %C', + $api->getClientCharset(), $api->getUser(), $flag_password, $host, From 0943561dcb780596a13a6c34b9090bfebe825dfa Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 27 Aug 2019 07:05:11 -0700 Subject: [PATCH 118/122] Fix incorrect construction of subtype map when validating "subtype" transactions against non-subtypable objects Summary: Fixes T13389. Currently, we try to "newSubtypeMap()" unconditionally, even if the underlying object does not support subtypes. - Only try to build a subtype map if subtype transactions are actually being applied. - When subtype transactions are applied to a non-subtypable object, fail more explicitly. Test Plan: Clicked "Make Editable" in a fresh Calendar transaction form, got an editable form instead of a fatal from "newSubtypeMap()". (Calendar events are not currently subtypable.) Maniphest Tasks: T13389 Differential Revision: https://secure.phabricator.com/D20741 --- ...habricatorEditEngineSubtypeTransaction.php | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php b/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php index 53ec221631..a2a8538115 100644 --- a/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php +++ b/src/applications/transactions/xaction/PhabricatorEditEngineSubtypeTransaction.php @@ -25,11 +25,30 @@ final class PhabricatorEditEngineSubtypeTransaction } public function validateTransactions($object, array $xactions) { - $map = $object->getEngine() + $errors = array(); + + if (!$xactions) { + return $errors; + } + + $engine = $object->getEngine(); + + if (!$engine->supportsSubtypes()) { + foreach ($xactions as $xaction) { + $errors[] = $this->newInvalidError( + pht( + 'Edit engine (of class "%s") does not support subtypes, so '. + 'subtype transactions can not be applied to it.', + get_class($engine)), + $xaction); + } + return $errors; + } + + $map = $engine ->setViewer($this->getActor()) ->newSubtypeMap(); - $errors = array(); foreach ($xactions as $xaction) { $new = $xaction->getNewValue(); From c6642213d57f82dda6927001312fcbde5a18c34e Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 28 Aug 2019 07:34:03 -0700 Subject: [PATCH 119/122] Straighten out replication/cache behavior in "bin/storage dump" Summary: Fixes T13336. - Prevent `--no-indexes` from being combined with `--for-replica`, since combining these options can only lead to heartbreak. - In `--for-replica` mode, dump caches too. See discussion in T13336. It is probably "safe" to not dump these today, but fragile and not correct. - Mark the "MarkupCache" table as having "Cache" persistence, not "Data" persistence (no need to back it up, since it can be fully regenerated from other datasources). Test Plan: Ran `bin/storage dump` with various combinations of flags. Maniphest Tasks: T13336 Differential Revision: https://secure.phabricator.com/D20743 --- .../cache/storage/PhabricatorMarkupCache.php | 4 ++++ .../schema/PhabricatorConfigSchemaSpec.php | 10 +++++++- src/infrastructure/storage/lisk/LiskDAO.php | 4 ++++ ...abricatorStorageManagementDumpWorkflow.php | 23 ++++++++++++++----- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/applications/cache/storage/PhabricatorMarkupCache.php b/src/applications/cache/storage/PhabricatorMarkupCache.php index 03a4b08681..e008a18ee1 100644 --- a/src/applications/cache/storage/PhabricatorMarkupCache.php +++ b/src/applications/cache/storage/PhabricatorMarkupCache.php @@ -30,4 +30,8 @@ final class PhabricatorMarkupCache extends PhabricatorCacheDAO { ) + parent::getConfiguration(); } + public function getSchemaPersistence() { + return PhabricatorConfigTableSchema::PERSISTENCE_CACHE; + } + } diff --git a/src/applications/config/schema/PhabricatorConfigSchemaSpec.php b/src/applications/config/schema/PhabricatorConfigSchemaSpec.php index 8e67391b59..adccdc1267 100644 --- a/src/applications/config/schema/PhabricatorConfigSchemaSpec.php +++ b/src/applications/config/schema/PhabricatorConfigSchemaSpec.php @@ -48,11 +48,19 @@ abstract class PhabricatorConfigSchemaSpec extends Phobject { abstract public function buildSchemata(); protected function buildLiskObjectSchema(PhabricatorLiskDAO $object) { + $index_options = array(); + + $persistence = $object->getSchemaPersistence(); + if ($persistence !== null) { + $index_options['persistence'] = $persistence; + } + $this->buildRawSchema( $object->getApplicationName(), $object->getTableName(), $object->getSchemaColumns(), - $object->getSchemaKeys()); + $object->getSchemaKeys(), + $index_options); } protected function buildFerretIndexSchema(PhabricatorFerretEngine $engine) { diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index 81005ab30d..0bbbdd83a7 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1881,6 +1881,10 @@ abstract class LiskDAO extends Phobject ->getMaximumByteLengthForDataType($data_type); } + public function getSchemaPersistence() { + return null; + } + /* -( AphrontDatabaseTableRefInterface )----------------------------------- */ diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index 3a18578a30..ebe1c77f40 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -14,7 +14,8 @@ final class PhabricatorStorageManagementDumpWorkflow 'name' => 'for-replica', 'help' => pht( 'Add __--master-data__ to the __mysqldump__ command, '. - 'generating a CHANGE MASTER statement in the output.'), + 'generating a CHANGE MASTER statement in the output. This '. + 'option also dumps all data, including caches.'), ), array( 'name' => 'output', @@ -54,6 +55,8 @@ final class PhabricatorStorageManagementDumpWorkflow $output_file = $args->getArg('output'); $is_compress = $args->getArg('compress'); $is_overwrite = $args->getArg('overwrite'); + $is_noindex = $args->getArg('no-indexes'); + $is_replica = $args->getArg('for-replica'); if ($is_compress) { if ($output_file === null) { @@ -79,6 +82,14 @@ final class PhabricatorStorageManagementDumpWorkflow } } + if ($is_replica && $is_noindex) { + throw new PhutilArgumentUsageException( + pht( + 'The "--for-replica" flag can not be used with the '. + '"--no-indexes" flag. Replication dumps must contain a complete '. + 'representation of database state.')); + } + if ($output_file !== null) { if (Filesystem::pathExists($output_file)) { if (!$is_overwrite) { @@ -94,8 +105,6 @@ final class PhabricatorStorageManagementDumpWorkflow $api = $this->getSingleAPI(); $patches = $this->getPatches(); - $with_indexes = !$args->getArg('no-indexes'); - $applied = $api->getAppliedPatches(); if ($applied === null) { throw new PhutilArgumentUsageException( @@ -119,6 +128,9 @@ final class PhabricatorStorageManagementDumpWorkflow $schemata = $actual_map[$ref_key]; $expect = $expect_map[$ref_key]; + $with_caches = $is_replica; + $with_indexes = !$is_noindex; + $targets = array(); foreach ($schemata->getDatabases() as $database_name => $database) { $expect_database = $expect->getDatabase($database_name); @@ -143,7 +155,7 @@ final class PhabricatorStorageManagementDumpWorkflow // When dumping tables, leave the data in cache tables in the // database. This will be automatically rebuild after the data // is restored and does not need to be persisted in backups. - $with_data = false; + $with_data = $with_caches; break; case PhabricatorConfigTableSchema::PERSISTENCE_INDEX: // When dumping tables, leave index data behind of the caller @@ -183,7 +195,7 @@ final class PhabricatorStorageManagementDumpWorkflow $argv[] = '--default-character-set'; $argv[] = $api->getClientCharset(); - if ($args->getArg('for-replica')) { + if ($is_replica) { $argv[] = '--master-data'; } @@ -342,7 +354,6 @@ final class PhabricatorStorageManagementDumpWorkflow return 0; } - private function writeData($data, $file, $is_compress, $output_file) { if (!strlen($data)) { return; From 3c26e384872ac8c1193b304434428eb3b42ff2f7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 29 Aug 2019 12:55:39 -0700 Subject: [PATCH 120/122] Provide a simple read-only maintenance mode for repositories Summary: Ref T13393. While doing a shard migration in the Phacility cluster, we'd like to stop writes to the migrating repository. It's safe to continue serving reads. Add a simple maintenance mode for making repositories completely read-only during maintenance. Test Plan: Put a repository into read-only mode, tried to write via HTTP + SSH. Viewed web UI. Took it back out of maintenance mode. Maniphest Tasks: T13393 Differential Revision: https://secure.phabricator.com/D20748 --- src/__phutil_library_map__.php | 4 + .../DiffusionRepositoryController.php | 25 ++++- .../controller/DiffusionServeController.php | 6 + .../diffusion/ssh/DiffusionSSHWorkflow.php | 4 + .../PhabricatorRepositoryPullEngine.php | 7 ++ ...epositoryManagementMaintenanceWorkflow.php | 104 ++++++++++++++++++ .../storage/PhabricatorRepository.php | 35 ++++++ ...icatorRepositoryMaintenanceTransaction.php | 43 ++++++++ 8 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php create mode 100644 src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 81ac776d5d..5d9a0aed3c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4470,6 +4470,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryIdentityTransaction' => 'applications/repository/storage/PhabricatorRepositoryIdentityTransaction.php', 'PhabricatorRepositoryIdentityTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryIdentityTransactionQuery.php', 'PhabricatorRepositoryIdentityTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryIdentityTransactionType.php', + 'PhabricatorRepositoryMaintenanceTransaction' => 'applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php', 'PhabricatorRepositoryManagementCacheWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementCacheWorkflow.php', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', @@ -4478,6 +4479,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementListPathsWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListPathsWorkflow.php', 'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementLookupUsersWorkflow.php', + 'PhabricatorRepositoryManagementMaintenanceWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkImportedWorkflow.php', 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMarkReachableWorkflow.php', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementMirrorWorkflow.php', @@ -10911,6 +10913,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryIdentityTransaction' => 'PhabricatorModularTransaction', 'PhabricatorRepositoryIdentityTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorRepositoryIdentityTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorRepositoryMaintenanceTransaction' => 'PhabricatorRepositoryTransactionType', 'PhabricatorRepositoryManagementCacheWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementClusterizeWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', @@ -10919,6 +10922,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryManagementListPathsWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementLookupUsersWorkflow' => 'PhabricatorRepositoryManagementWorkflow', + 'PhabricatorRepositoryManagementMaintenanceWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkImportedWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMarkReachableWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementMirrorWorkflow' => 'PhabricatorRepositoryManagementWorkflow', diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index 12aaefe12a..f00521257e 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -145,13 +145,26 @@ final class DiffusionRepositoryController extends DiffusionController { ->setRight(array($this->branchButton, $actions_button, $clone_button)) ->addClass('diffusion-action-bar'); + $status_view = null; + if ($repository->isReadOnly()) { + $status_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors( + array( + phutil_escape_html_newlines( + $repository->getReadOnlyMessageForDisplay()), + )); + } + $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $bar, - $description, - $content, - )); + ->setFooter( + array( + $status_view, + $bar, + $description, + $content, + )); if ($page_has_content) { $view->setTabs($tabs); @@ -327,6 +340,8 @@ final class DiffusionRepositoryController extends DiffusionController { if (!$repository->isTracked()) { $header->setStatus('fa-ban', 'dark', pht('Inactive')); + } else if ($repository->isReadOnly()) { + $header->setStatus('fa-wrench', 'indigo', pht('Under Maintenance')); } else if ($repository->isImporting()) { $ratio = $repository->loadImportProgress(); $percentage = sprintf('%.2f%%', 100 * $ratio); diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index aea901f100..60d5c1578d 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -302,6 +302,12 @@ final class DiffusionServeController extends DiffusionController { } if ($is_push) { + if ($repository->isReadOnly()) { + return new PhabricatorVCSResponse( + 503, + $repository->getReadOnlyMessageForDisplay()); + } + $can_write = $repository->canServeProtocol($proto_https, true) || $repository->canServeProtocol($proto_http, true); diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 57dc83953d..08144eb0c9 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -255,6 +255,10 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { 'user account.')); } + if ($repository->isReadOnly()) { + throw new Exception($repository->getReadOnlyMessageForDisplay()); + } + $protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; if ($repository->canServeProtocol($protocol, true)) { $can_push = PhabricatorPolicyFilter::hasCapability( diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 2c6ac8e83f..ea70f380aa 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -52,6 +52,13 @@ final class PhabricatorRepositoryPullEngine $repository = $this->getRepository(); $viewer = PhabricatorUser::getOmnipotentUser(); + if ($repository->isReadOnly()) { + $this->skipPull( + pht( + "Skipping pull on read-only repository.\n\n%s", + $repository->getReadOnlyMessageForDisplay())); + } + $is_hg = false; $is_git = false; $is_svn = false; diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php new file mode 100644 index 0000000000..65fe0adaad --- /dev/null +++ b/src/applications/repository/management/PhabricatorRepositoryManagementMaintenanceWorkflow.php @@ -0,0 +1,104 @@ +setName('maintenance') + ->setExamples( + "**maintenance** --start __message__ __repository__ ...\n". + "**maintenance** --stop __repository__") + ->setSynopsis( + pht('Set or clear read-only mode for repository maintenance.')) + ->setArguments( + array( + array( + 'name' => 'start', + 'param' => 'message', + 'help' => pht( + 'Put repositories into maintenance mode.'), + ), + array( + 'name' => 'stop', + 'help' => pht( + 'Take repositories out of maintenance mode, returning them '. + 'to normal serice.'), + ), + array( + 'name' => 'repositories', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $repositories = $this->loadRepositories($args, 'repositories'); + if (!$repositories) { + throw new PhutilArgumentUsageException( + pht('Specify one or more repositories to act on.')); + } + + $message = $args->getArg('start'); + $is_start = (bool)strlen($message); + $is_stop = $args->getArg('stop'); + + if (!$is_start && !$is_stop) { + throw new PhutilArgumentUsageException( + pht( + 'Use "--start " to put repositories into maintenance '. + 'mode, or "--stop" to take them out of maintenance mode.')); + } + + if ($is_start && $is_stop) { + throw new PhutilArgumentUsageException( + pht( + 'Specify either "--start" or "--stop", but not both.')); + } + + $content_source = $this->newContentSource(); + $diffusion_phid = id(new PhabricatorDiffusionApplication())->getPHID(); + + if ($is_start) { + $new_value = $message; + } else { + $new_value = null; + } + + foreach ($repositories as $repository) { + $xactions = array(); + + $xactions[] = $repository->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorRepositoryMaintenanceTransaction::TRANSACTIONTYPE) + ->setNewValue($new_value); + + $repository->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($diffusion_phid) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($repository, $xactions); + + if ($is_start) { + echo tsprintf( + "%s\n", + pht( + 'Put repository "%s" into maintenance mode.', + $repository->getDisplayName())); + } else { + echo tsprintf( + "%s\n", + pht( + 'Took repository "%s" out of maintenance mode.', + $repository->getDisplayName())); + } + } + + return 0; + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 9661584851..bd15e3e8de 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1410,6 +1410,12 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } } + if ($write) { + if ($this->isReadOnly()) { + return false; + } + } + return false; } @@ -2266,6 +2272,35 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return $this->isGit(); } + public function isReadOnly() { + return (bool)$this->getDetail('read-only'); + } + + public function setReadOnly($read_only) { + return $this->setDetail('read-only', $read_only); + } + + public function getReadOnlyMessage() { + return $this->getDetail('read-only-message'); + } + + public function setReadOnlyMessage($message) { + return $this->setDetail('read-only-message', $message); + } + + public function getReadOnlyMessageForDisplay() { + $parts = array(); + $parts[] = pht( + 'This repository is currently in read-only maintenance mode.'); + + $message = $this->getReadOnlyMessage(); + if ($message !== null) { + $parts[] = $message; + } + + return implode("\n\n", $parts); + } + /* -( Repository URIs )---------------------------------------------------- */ diff --git a/src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php new file mode 100644 index 0000000000..caf9e84527 --- /dev/null +++ b/src/applications/repository/xaction/PhabricatorRepositoryMaintenanceTransaction.php @@ -0,0 +1,43 @@ +getReadOnlyMessage(); + } + + public function applyInternalEffects($object, $value) { + if ($value === null) { + $object + ->setReadOnly(false) + ->setReadOnlyMessage(null); + } else { + $object + ->setReadOnly(true) + ->setReadOnlyMessage($value); + } + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && !strlen($new)) { + return pht( + '%s took this repository out of maintenance mode.', + $this->renderAuthor()); + } else if (!strlen($old) && strlen($new)) { + return pht( + '%s put this repository into maintenance mode.', + $this->renderAuthor()); + } else { + return pht( + '%s updated the maintenance message for this repository.', + $this->renderAuthor()); + } + } + +} From 533a5535b6f0104c13266400735cb563d64525e7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 30 Aug 2019 09:12:57 -0700 Subject: [PATCH 121/122] Remove the "grant authority" mechanism from users Summary: Ref T13393. See some previous discussion in T13366. Caching is hard and all approaches here have downsides, but the request cache likely has fewer practical downsides for this kind of policy check than other approaches. In particular, the grant approach (at least, as previously used in Phortune) has a major downside that "Query" classes can no longer fully enforce policies. Since Phortune no longer depends on grants and they've now been removed from instances, drop the mechanism completely. Test Plan: Grepped for callsites, found none. Maniphest Tasks: T13393 Differential Revision: https://secure.phabricator.com/D20754 --- .../people/storage/PhabricatorUser.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index c675878747..cce4ffa58a 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -59,7 +59,6 @@ final class PhabricatorUser private $rawCacheData = array(); private $usableCacheData = array(); - private $authorities = array(); private $handlePool; private $csrfSalt; @@ -705,23 +704,6 @@ final class PhabricatorUser } - /** - * Grant a user a source of authority, to let them bypass policy checks they - * could not otherwise. - */ - public function grantAuthority($authority) { - $this->authorities[] = $authority; - return $this; - } - - - /** - * Get authorities granted to the user. - */ - public function getAuthorities() { - return $this->authorities; - } - public function hasConduitClusterToken() { return ($this->conduitClusterToken !== self::ATTACHABLE); } From b2b17485b9571b118ff5e9e5fb80af3dbae64ea6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 31 Aug 2019 09:36:23 -0700 Subject: [PATCH 122/122] Clean up two straggling UI issues in Phortune Ref T13401. The checkout UI didn't get fully updated to the new View objects, and account handles are still manually building a URI that goes to the wrong place. --- .../controller/cart/PhortuneCartCheckoutController.php | 10 +++------- .../phortune/phid/PhortuneAccountPHIDType.php | 7 +++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php index 793187c404..cbd434c900 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php @@ -101,13 +101,9 @@ final class PhortuneCartCheckoutController } } - $cart_table = $this->buildCartContentTable($cart); - - $cart_box = id(new PHUIObjectBoxView()) - ->setFormErrors($errors) - ->setHeaderText(pht('Cart Contents')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($cart_table); + $cart_box = id(new PhortuneOrderItemsView()) + ->setViewer($viewer) + ->setOrder($cart); $title = $cart->getName(); diff --git a/src/applications/phortune/phid/PhortuneAccountPHIDType.php b/src/applications/phortune/phid/PhortuneAccountPHIDType.php index cf5f5d06f2..90632a980d 100644 --- a/src/applications/phortune/phid/PhortuneAccountPHIDType.php +++ b/src/applications/phortune/phid/PhortuneAccountPHIDType.php @@ -32,10 +32,9 @@ final class PhortuneAccountPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $account = $objects[$phid]; - $id = $account->getID(); - - $handle->setName($account->getName()); - $handle->setURI("/phortune/{$id}/"); + $handle + ->setName($account->getName()) + ->setURI($account->getURI()); } }